Numeric range markets!! (#146)
* Numeric contract type * Create market numeric type * Add numeric graph (coded without testing) * Outline of numeric bet panel * Update bet panel logic * create numeric contracts * remove batching for antes for numeric markets * Remove focus * numeric market range [1, 100] * Zoom graph * Hide bet panels * getNumericBets * Add numeric resolution panel * Use getNumericBets in bet panel calc * Switch bucket count to 100 * Parallelize ante creation * placeBet for numeric markets * halve std of numeric bets * Update resolveMarket with numeric type * Set min and max for contract * lower std for numeric bets * calculateNumericDpmShares: use sorted order * Use min and max to map the input * Fix probability calc * normpdf variance mislabeled * range input * merge * change numeric graph color * fix getNewContract params * bet panel labels * validation * number input * fix bucketing * bucket input, numeric resolution panel * outcome label * merge * numeric bet panel on mobile * Make text underneath filled green answer bar selectable * Default to 'all' feed category when loading page. * fix numeric resolution panel * fix numeric bet panel calculations * display numeric resolution * don't render NumericBetPanel for non numeric markets * numeric bets: store shares, bet amounts across buckets in each bet object * restore your bets for numeric markets * numeric pnl calculations * remove hasUserHitManaLimit * contrain contract type * handle undefined allOutcomeShares * numeric ante bet amount * use correct amount for numeric dpm payouts * change numeric graph/outcome color * numeric constants * hack to show correct numeric payout in calculateDpmPayoutAfterCorrectBet * remove comment * fix ante display in bet list * halve bucket count * cast to NumericContract * fix merge imports * OUTCOME_TYPES * typo * lower bucket count to 200 * store raw numeric value with bet * store raw numeric resolution value * number input max length * create page: min, max to undefined if not numeric market * numeric resolution formatting * expected value for numeric markets * expected value for numeric markets * rearrange lines for readability * move normalpdf to util/math * show bets tab * check if outcomeMode is undefined * remove extraneous auto-merge cruft * hide comment status for numeric markets * import Co-authored-by: mantikoros <sgrugett@gmail.com>
This commit is contained in:
parent
7d8ccb78a4
commit
76f27d1a93
|
@ -1,9 +1,17 @@
|
|||
import { Bet } from './bet'
|
||||
import { getDpmProbability } from './calculate-dpm'
|
||||
import { Binary, CPMM, DPM, FreeResponse, FullContract } from './contract'
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import { getDpmProbability, getValueFromBucket } from './calculate-dpm'
|
||||
import {
|
||||
Binary,
|
||||
CPMM,
|
||||
DPM,
|
||||
FreeResponse,
|
||||
FullContract,
|
||||
Numeric,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import { noFees } from './fees'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
export const FIXED_ANTE = 100
|
||||
|
||||
|
@ -106,3 +114,42 @@ export function getFreeAnswerAnte(
|
|||
|
||||
return anteBet
|
||||
}
|
||||
|
||||
export function getNumericAnte(
|
||||
creator: User,
|
||||
contract: FullContract<DPM, Numeric>,
|
||||
ante: number,
|
||||
newBetId: string
|
||||
) {
|
||||
const { bucketCount, createdTime } = contract
|
||||
|
||||
const betAnte = ante / bucketCount
|
||||
const betShares = Math.sqrt(ante ** 2 / bucketCount)
|
||||
|
||||
const allOutcomeShares = Object.fromEntries(
|
||||
_.range(0, bucketCount).map((_, i) => [i, betShares])
|
||||
)
|
||||
|
||||
const allBetAmounts = Object.fromEntries(
|
||||
_.range(0, bucketCount).map((_, i) => [i, betAnte])
|
||||
)
|
||||
|
||||
const anteBet: NumericBet = {
|
||||
id: newBetId,
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
amount: ante,
|
||||
allBetAmounts,
|
||||
outcome: '0',
|
||||
value: getValueFromBucket('0', contract),
|
||||
shares: betShares,
|
||||
allOutcomeShares,
|
||||
probBefore: 0,
|
||||
probAfter: 1 / bucketCount,
|
||||
createdTime,
|
||||
isAnte: true,
|
||||
fees: noFees,
|
||||
}
|
||||
|
||||
return anteBet
|
||||
}
|
||||
|
|
|
@ -29,4 +29,10 @@ export type Bet = {
|
|||
createdTime: number
|
||||
}
|
||||
|
||||
export type NumericBet = Bet & {
|
||||
value: number
|
||||
allOutcomeShares: { [outcome: string]: number }
|
||||
allBetAmounts: { [outcome: string]: number }
|
||||
}
|
||||
|
||||
export const MAX_LOAN_PER_CONTRACT = 20
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
import * as _ from 'lodash'
|
||||
import { Bet } from './bet'
|
||||
import { Binary, DPM, FreeResponse, FullContract } from './contract'
|
||||
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import {
|
||||
Binary,
|
||||
DPM,
|
||||
FreeResponse,
|
||||
FullContract,
|
||||
Numeric,
|
||||
NumericContract,
|
||||
} from './contract'
|
||||
import { DPM_FEES } from './fees'
|
||||
import { normpdf } from '../common/util/math'
|
||||
import { addObjects } from './util/object'
|
||||
|
||||
export function getDpmProbability(totalShares: { [outcome: string]: number }) {
|
||||
// For binary contracts only.
|
||||
|
@ -19,6 +29,91 @@ export function getDpmOutcomeProbability(
|
|||
return shares ** 2 / squareSum
|
||||
}
|
||||
|
||||
export function getDpmOutcomeProbabilities(totalShares: {
|
||||
[outcome: string]: number
|
||||
}) {
|
||||
const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||
return _.mapValues(totalShares, (shares) => shares ** 2 / squareSum)
|
||||
}
|
||||
|
||||
export function getNumericBets(
|
||||
contract: NumericContract,
|
||||
bucket: string,
|
||||
betAmount: number,
|
||||
variance: number
|
||||
) {
|
||||
const { bucketCount } = contract
|
||||
const bucketNumber = parseInt(bucket)
|
||||
const buckets = _.range(0, bucketCount)
|
||||
|
||||
const mean = bucketNumber / bucketCount
|
||||
|
||||
const allDensities = buckets.map((i) =>
|
||||
normpdf(i / bucketCount, mean, variance)
|
||||
)
|
||||
const densitySum = _.sum(allDensities)
|
||||
|
||||
const rawBetAmounts = allDensities
|
||||
.map((d) => (d / densitySum) * betAmount)
|
||||
.map((x) => (x >= 1 / bucketCount ? x : 0))
|
||||
|
||||
const rawSum = _.sum(rawBetAmounts)
|
||||
const scaledBetAmounts = rawBetAmounts.map((x) => (x / rawSum) * betAmount)
|
||||
|
||||
const bets = scaledBetAmounts
|
||||
.map((x, i) => (x > 0 ? [i.toString(), x] : undefined))
|
||||
.filter((x) => x != undefined) as [string, number][]
|
||||
|
||||
return bets
|
||||
}
|
||||
|
||||
export const getMappedBucket = (value: number, contract: NumericContract) => {
|
||||
const { bucketCount, min, max } = contract
|
||||
|
||||
const index = Math.floor(((value - min) / (max - min)) * bucketCount)
|
||||
const bucket = Math.max(Math.min(index, bucketCount - 1), 0)
|
||||
|
||||
return `${bucket}`
|
||||
}
|
||||
|
||||
export const getValueFromBucket = (
|
||||
bucket: string,
|
||||
contract: NumericContract
|
||||
) => {
|
||||
const { bucketCount, min, max } = contract
|
||||
const index = parseInt(bucket)
|
||||
const value = min + (index / bucketCount) * (max - min)
|
||||
const rounded = Math.round(value * 1e4) / 1e4
|
||||
return rounded
|
||||
}
|
||||
|
||||
export const getExpectedValue = (contract: NumericContract) => {
|
||||
const { bucketCount, min, max, totalShares } = contract
|
||||
|
||||
const totalShareSum = _.sumBy(
|
||||
Object.values(totalShares),
|
||||
(shares) => shares ** 2
|
||||
)
|
||||
const probs = _.range(0, bucketCount).map(
|
||||
(i) => totalShares[i] ** 2 / totalShareSum
|
||||
)
|
||||
|
||||
const values = _.range(0, bucketCount).map(
|
||||
(i) =>
|
||||
// use mid point within bucket
|
||||
0.5 * (min + (i / bucketCount) * (max - min)) +
|
||||
0.5 * (min + ((i + 1) / bucketCount) * (max - min))
|
||||
)
|
||||
|
||||
const weightedValues = _.range(0, bucketCount).map(
|
||||
(i) => probs[i] * values[i]
|
||||
)
|
||||
|
||||
const expectation = _.sum(weightedValues)
|
||||
const rounded = Math.round(expectation * 1e2) / 1e2
|
||||
return rounded
|
||||
}
|
||||
|
||||
export function getDpmOutcomeProbabilityAfterBet(
|
||||
totalShares: {
|
||||
[outcome: string]: number
|
||||
|
@ -63,6 +158,30 @@ export function calculateDpmShares(
|
|||
return Math.sqrt(bet ** 2 + shares ** 2 + c) - shares
|
||||
}
|
||||
|
||||
export function calculateNumericDpmShares(
|
||||
totalShares: {
|
||||
[outcome: string]: number
|
||||
},
|
||||
bets: [string, number][]
|
||||
) {
|
||||
const shares: number[] = []
|
||||
|
||||
totalShares = _.cloneDeep(totalShares)
|
||||
|
||||
const order = _.sortBy(
|
||||
bets.map(([, amount], i) => [amount, i]),
|
||||
([amount]) => amount
|
||||
).map(([, i]) => i)
|
||||
|
||||
for (let i of order) {
|
||||
const [bucket, bet] = bets[i]
|
||||
shares[i] = calculateDpmShares(totalShares, bet, bucket)
|
||||
totalShares = addObjects(totalShares, { [bucket]: shares[i] })
|
||||
}
|
||||
|
||||
return { shares, totalShares }
|
||||
}
|
||||
|
||||
export function calculateDpmRawShareValue(
|
||||
totalShares: {
|
||||
[outcome: string]: number
|
||||
|
@ -163,8 +282,15 @@ export function calculateStandardDpmPayout(
|
|||
bet: Bet,
|
||||
outcome: string
|
||||
) {
|
||||
const { amount, outcome: betOutcome, shares } = bet
|
||||
if (betOutcome !== outcome) return 0
|
||||
const { outcome: betOutcome } = bet
|
||||
const isNumeric = contract.outcomeType === 'NUMERIC'
|
||||
if (!isNumeric && betOutcome !== outcome) return 0
|
||||
|
||||
const shares = isNumeric
|
||||
? ((bet as NumericBet).allOutcomeShares ?? {})[outcome]
|
||||
: bet.shares
|
||||
|
||||
if (!shares) return 0
|
||||
|
||||
const { totalShares, phantomShares, pool } = contract
|
||||
if (!totalShares[outcome]) return 0
|
||||
|
@ -175,15 +301,20 @@ export function calculateStandardDpmPayout(
|
|||
totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0)
|
||||
|
||||
const winnings = (shares / total) * poolTotal
|
||||
// profit can be negative if using phantom shares
|
||||
return amount + (1 - DPM_FEES) * Math.max(0, winnings - amount)
|
||||
|
||||
const amount = isNumeric
|
||||
? (bet as NumericBet).allBetAmounts[outcome]
|
||||
: bet.amount
|
||||
|
||||
const payout = amount + (1 - DPM_FEES) * Math.max(0, winnings - amount)
|
||||
return payout
|
||||
}
|
||||
|
||||
export function calculateDpmPayoutAfterCorrectBet(
|
||||
contract: FullContract<DPM, any>,
|
||||
bet: Bet
|
||||
) {
|
||||
const { totalShares, pool, totalBets } = contract
|
||||
const { totalShares, pool, totalBets, outcomeType } = contract
|
||||
const { shares, amount, outcome } = bet
|
||||
|
||||
const prevShares = totalShares[outcome] ?? 0
|
||||
|
@ -204,19 +335,23 @@ export function calculateDpmPayoutAfterCorrectBet(
|
|||
...totalBets,
|
||||
[outcome]: prevTotalBet + amount,
|
||||
},
|
||||
outcomeType:
|
||||
outcomeType === 'NUMERIC'
|
||||
? 'FREE_RESPONSE' // hack to show payout at particular bet point estimate
|
||||
: outcomeType,
|
||||
}
|
||||
|
||||
return calculateStandardDpmPayout(newContract, bet, outcome)
|
||||
}
|
||||
|
||||
function calculateMktDpmPayout(contract: FullContract<DPM, any>, bet: Bet) {
|
||||
function calculateMktDpmPayout(
|
||||
contract: FullContract<DPM, Binary | FreeResponse | Numeric>,
|
||||
bet: Bet
|
||||
) {
|
||||
if (contract.outcomeType === 'BINARY')
|
||||
return calculateBinaryMktDpmPayout(contract, bet)
|
||||
|
||||
const { totalShares, pool, resolutions } = contract as FullContract<
|
||||
DPM,
|
||||
FreeResponse
|
||||
>
|
||||
const { totalShares, pool, resolutions, outcomeType } = contract
|
||||
|
||||
let probs: { [outcome: string]: number }
|
||||
|
||||
|
@ -239,10 +374,21 @@ function calculateMktDpmPayout(contract: FullContract<DPM, any>, bet: Bet) {
|
|||
|
||||
const { outcome, amount, shares } = bet
|
||||
|
||||
const totalPool = _.sum(Object.values(pool))
|
||||
const poolFrac = (probs[outcome] * shares) / weightedShareTotal
|
||||
const winnings = poolFrac * totalPool
|
||||
const poolFrac =
|
||||
outcomeType === 'NUMERIC'
|
||||
? _.sumBy(
|
||||
Object.keys((bet as NumericBet).allOutcomeShares ?? {}),
|
||||
(outcome) => {
|
||||
return (
|
||||
(probs[outcome] * (bet as NumericBet).allOutcomeShares[outcome]) /
|
||||
weightedShareTotal
|
||||
)
|
||||
}
|
||||
)
|
||||
: (probs[outcome] * shares) / weightedShareTotal
|
||||
|
||||
const totalPool = _.sum(Object.values(pool))
|
||||
const winnings = poolFrac * totalPool
|
||||
return deductDpmFees(amount, winnings)
|
||||
}
|
||||
|
||||
|
|
|
@ -161,7 +161,6 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
|
||||
return {
|
||||
invested: Math.max(0, currentInvested),
|
||||
currentInvested,
|
||||
payout,
|
||||
netPayout,
|
||||
profit,
|
||||
|
@ -190,29 +189,3 @@ export function getTopAnswer(contract: FreeResponseContract) {
|
|||
)
|
||||
return top?.answer
|
||||
}
|
||||
|
||||
export function hasUserHitManaLimit(
|
||||
contract: FreeResponseContract,
|
||||
bets: Bet[],
|
||||
amount: number
|
||||
) {
|
||||
const { manaLimitPerUser } = contract
|
||||
if (manaLimitPerUser) {
|
||||
const contractMetrics = getContractBetMetrics(contract, bets)
|
||||
const currentInvested = contractMetrics.currentInvested
|
||||
console.log('user current invested amount', currentInvested)
|
||||
console.log('mana limit:', manaLimitPerUser)
|
||||
|
||||
if (currentInvested + amount > manaLimitPerUser) {
|
||||
const manaAllowed = manaLimitPerUser - currentInvested
|
||||
return {
|
||||
status: 'error',
|
||||
message: `Market bet cap is M$${manaLimitPerUser}, you've M$${manaAllowed} left`,
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 'success',
|
||||
message: '',
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import * as _ from 'lodash'
|
||||
import { Answer } from './answer'
|
||||
import { Fees } from './fees'
|
||||
|
||||
export type FullContract<
|
||||
M extends DPM | CPMM,
|
||||
T extends Binary | Multi | FreeResponse
|
||||
T extends Binary | Multi | FreeResponse | Numeric
|
||||
> = {
|
||||
id: string
|
||||
slug: string // auto-generated; must be unique
|
||||
|
@ -11,7 +12,7 @@ export type FullContract<
|
|||
creatorId: string
|
||||
creatorName: string
|
||||
creatorUsername: string
|
||||
creatorAvatarUrl?: string // Start requiring after 2022-03-01
|
||||
creatorAvatarUrl?: string
|
||||
|
||||
question: string
|
||||
description: string // More info about what the contract is about
|
||||
|
@ -41,9 +42,13 @@ export type FullContract<
|
|||
} & M &
|
||||
T
|
||||
|
||||
export type Contract = FullContract<DPM | CPMM, Binary | Multi | FreeResponse>
|
||||
export type Contract = FullContract<
|
||||
DPM | CPMM,
|
||||
Binary | Multi | FreeResponse | Numeric
|
||||
>
|
||||
export type BinaryContract = FullContract<DPM | CPMM, Binary>
|
||||
export type FreeResponseContract = FullContract<DPM | CPMM, FreeResponse>
|
||||
export type NumericContract = FullContract<DPM, Numeric>
|
||||
|
||||
export type DPM = {
|
||||
mechanism: 'dpm-2'
|
||||
|
@ -83,7 +88,17 @@ export type FreeResponse = {
|
|||
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||
}
|
||||
|
||||
export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE'
|
||||
export type Numeric = {
|
||||
outcomeType: 'NUMERIC'
|
||||
bucketCount: number
|
||||
min: number
|
||||
max: number
|
||||
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||
resolutionValue?: number
|
||||
}
|
||||
|
||||
export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE' | 'NUMERIC'
|
||||
export const OUTCOME_TYPES = ['BINARY', 'MULTI', 'FREE_RESPONSE', 'NUMERIC']
|
||||
|
||||
export const MAX_QUESTION_LENGTH = 480
|
||||
export const MAX_DESCRIPTION_LENGTH = 10000
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { Bet, MAX_LOAN_PER_CONTRACT } from './bet'
|
||||
import { Bet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
|
||||
import {
|
||||
calculateDpmShares,
|
||||
getDpmProbability,
|
||||
getDpmOutcomeProbability,
|
||||
getNumericBets,
|
||||
calculateNumericDpmShares,
|
||||
} from './calculate-dpm'
|
||||
import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm'
|
||||
import {
|
||||
|
@ -14,9 +16,12 @@ import {
|
|||
FreeResponse,
|
||||
FullContract,
|
||||
Multi,
|
||||
NumericContract,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
import { noFees } from './fees'
|
||||
import { addObjects } from './util/object'
|
||||
import { NUMERIC_FIXED_VAR } from './numeric-constants'
|
||||
|
||||
export const getNewBinaryCpmmBetInfo = (
|
||||
user: User,
|
||||
|
@ -154,6 +159,55 @@ export const getNewMultiBetInfo = (
|
|||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||
}
|
||||
|
||||
export const getNumericBetsInfo = (
|
||||
user: User,
|
||||
value: number,
|
||||
outcome: string,
|
||||
amount: number,
|
||||
contract: NumericContract,
|
||||
newBetId: string
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
|
||||
const bets = getNumericBets(contract, outcome, amount, NUMERIC_FIXED_VAR)
|
||||
|
||||
const allBetAmounts = Object.fromEntries(bets)
|
||||
const newTotalBets = addObjects(totalBets, allBetAmounts)
|
||||
const newPool = addObjects(pool, allBetAmounts)
|
||||
|
||||
const { shares, totalShares: newTotalShares } = calculateNumericDpmShares(
|
||||
contract.totalShares,
|
||||
bets
|
||||
)
|
||||
|
||||
const allOutcomeShares = Object.fromEntries(
|
||||
bets.map(([outcome], i) => [outcome, shares[i]])
|
||||
)
|
||||
|
||||
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
|
||||
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
|
||||
|
||||
const newBet: NumericBet = {
|
||||
id: newBetId,
|
||||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
value,
|
||||
amount,
|
||||
allBetAmounts,
|
||||
shares: shares.find((s, i) => bets[i][0] === outcome) ?? 0,
|
||||
allOutcomeShares,
|
||||
outcome,
|
||||
probBefore,
|
||||
probAfter,
|
||||
createdTime: Date.now(),
|
||||
fees: noFees,
|
||||
}
|
||||
|
||||
const newBalance = user.balance - amount
|
||||
|
||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||
}
|
||||
|
||||
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
|
||||
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||
const prevLoanAmount = _.sumBy(openBets, (bet) => bet.loanAmount ?? 0)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { PHANTOM_ANTE } from './antes'
|
||||
import {
|
||||
Binary,
|
||||
|
@ -5,6 +7,7 @@ import {
|
|||
CPMM,
|
||||
DPM,
|
||||
FreeResponse,
|
||||
Numeric,
|
||||
outcomeType,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
|
@ -23,6 +26,11 @@ export function getNewContract(
|
|||
ante: number,
|
||||
closeTime: number,
|
||||
extraTags: string[],
|
||||
|
||||
// used for numeric markets
|
||||
bucketCount: number,
|
||||
min: number,
|
||||
max: number,
|
||||
manaLimitPerUser: number
|
||||
) {
|
||||
const tags = parseTags(
|
||||
|
@ -33,6 +41,8 @@ export function getNewContract(
|
|||
const propsByOutcomeType =
|
||||
outcomeType === 'BINARY'
|
||||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
||||
: outcomeType === 'NUMERIC'
|
||||
? getNumericProps(ante, bucketCount, min, max)
|
||||
: getFreeAnswerProps(ante)
|
||||
|
||||
const contract: Contract = removeUndefinedProps({
|
||||
|
@ -115,6 +125,37 @@ const getFreeAnswerProps = (ante: number) => {
|
|||
return system
|
||||
}
|
||||
|
||||
const getNumericProps = (
|
||||
ante: number,
|
||||
bucketCount: number,
|
||||
min: number,
|
||||
max: number
|
||||
) => {
|
||||
const buckets = _.range(0, bucketCount).map((i) => i.toString())
|
||||
|
||||
const betAnte = ante / bucketCount
|
||||
const pool = Object.fromEntries(buckets.map((answer) => [answer, betAnte]))
|
||||
const totalBets = pool
|
||||
|
||||
const betShares = Math.sqrt(ante ** 2 / bucketCount)
|
||||
const totalShares = Object.fromEntries(
|
||||
buckets.map((answer) => [answer, betShares])
|
||||
)
|
||||
|
||||
const system: DPM & Numeric = {
|
||||
mechanism: 'dpm-2',
|
||||
outcomeType: 'NUMERIC',
|
||||
pool,
|
||||
totalBets,
|
||||
totalShares,
|
||||
bucketCount,
|
||||
min,
|
||||
max,
|
||||
}
|
||||
|
||||
return system
|
||||
}
|
||||
|
||||
const getMultiProps = (
|
||||
outcomes: string[],
|
||||
initialProbs: number[],
|
||||
|
|
5
common/numeric-constants.ts
Normal file
5
common/numeric-constants.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const NUMERIC_BUCKET_COUNT = 200
|
||||
export const NUMERIC_FIXED_VAR = 0.005
|
||||
|
||||
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
||||
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
|
@ -1,6 +1,6 @@
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { Bet } from './bet'
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
||||
import { DPM, FreeResponse, FullContract, Multi } from './contract'
|
||||
import {
|
||||
|
@ -88,6 +88,64 @@ export const getDpmStandardPayouts = (
|
|||
}
|
||||
}
|
||||
|
||||
export const getNumericDpmPayouts = (
|
||||
outcome: string,
|
||||
contract: FullContract<DPM, any>,
|
||||
bets: NumericBet[]
|
||||
) => {
|
||||
const totalShares = _.sumBy(bets, (bet) => bet.allOutcomeShares[outcome] ?? 0)
|
||||
const winningBets = bets.filter((bet) => !!bet.allOutcomeShares[outcome])
|
||||
|
||||
const poolTotal = _.sum(Object.values(contract.pool))
|
||||
|
||||
const payouts = winningBets.map(
|
||||
({ userId, allBetAmounts, allOutcomeShares }) => {
|
||||
const shares = allOutcomeShares[outcome] ?? 0
|
||||
const winnings = (shares / totalShares) * poolTotal
|
||||
|
||||
const amount = allBetAmounts[outcome] ?? 0
|
||||
const profit = winnings - amount
|
||||
|
||||
// profit can be negative if using phantom shares
|
||||
const payout = amount + (1 - DPM_FEES) * Math.max(0, profit)
|
||||
return { userId, profit, payout }
|
||||
}
|
||||
)
|
||||
|
||||
const profits = _.sumBy(payouts, (po) => Math.max(0, po.profit))
|
||||
const creatorFee = DPM_CREATOR_FEE * profits
|
||||
const platformFee = DPM_PLATFORM_FEE * profits
|
||||
|
||||
const finalFees: Fees = {
|
||||
creatorFee,
|
||||
platformFee,
|
||||
liquidityFee: 0,
|
||||
}
|
||||
|
||||
const collectedFees = addObjects<Fees>(
|
||||
finalFees,
|
||||
contract.collectedFees ?? {}
|
||||
)
|
||||
|
||||
console.log(
|
||||
'resolved numeric bucket: ',
|
||||
outcome,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
liquidityPayouts: [],
|
||||
collectedFees,
|
||||
}
|
||||
}
|
||||
|
||||
export const getDpmMktPayouts = (
|
||||
contract: FullContract<DPM, any>,
|
||||
bets: Bet[],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { Bet } from './bet'
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import {
|
||||
Binary,
|
||||
Contract,
|
||||
|
@ -16,6 +16,7 @@ import {
|
|||
getDpmCancelPayouts,
|
||||
getDpmMktPayouts,
|
||||
getDpmStandardPayouts,
|
||||
getNumericDpmPayouts,
|
||||
getPayoutsMultiOutcome,
|
||||
} from './payouts-dpm'
|
||||
import {
|
||||
|
@ -131,6 +132,9 @@ export const getDpmPayouts = (
|
|||
return getDpmCancelPayouts(contract, openBets)
|
||||
|
||||
default:
|
||||
if (contract.outcomeType === 'NUMERIC')
|
||||
return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[])
|
||||
|
||||
// Outcome is a free response answer id.
|
||||
return getDpmStandardPayouts(outcome, contract, openBets)
|
||||
}
|
||||
|
|
|
@ -4,3 +4,16 @@ export const logInterpolation = (min: number, max: number, value: number) => {
|
|||
|
||||
return Math.log(value - min + 1) / Math.log(max - min + 1)
|
||||
}
|
||||
|
||||
export function normpdf(x: number, mean = 0, variance = 1) {
|
||||
if (variance === 0) {
|
||||
return x === mean ? Infinity : 0
|
||||
}
|
||||
|
||||
return (
|
||||
Math.exp((-0.5 * Math.pow(x - mean, 2)) / variance) /
|
||||
Math.sqrt(TAU * variance)
|
||||
)
|
||||
}
|
||||
|
||||
const TAU = Math.PI * 2
|
||||
|
|
|
@ -13,7 +13,6 @@ import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
|
|||
import { getContract, getValues } from './utils'
|
||||
import { sendNewAnswerEmail } from './emails'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { hasUserHitManaLimit } from '../../common/calculate'
|
||||
|
||||
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||
async (
|
||||
|
@ -67,13 +66,6 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
)
|
||||
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
|
||||
|
||||
const { status, message } = hasUserHitManaLimit(
|
||||
contract,
|
||||
yourBets,
|
||||
amount
|
||||
)
|
||||
if (status === 'error') return { status, message: message }
|
||||
|
||||
const [lastAnswer] = await getValues<Answer>(
|
||||
firestore
|
||||
.collection(`contracts/${contractId}/answers`)
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { chargeUser } from './utils'
|
||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
||||
import {
|
||||
Binary,
|
||||
Contract,
|
||||
|
@ -12,19 +10,27 @@ import {
|
|||
MAX_DESCRIPTION_LENGTH,
|
||||
MAX_QUESTION_LENGTH,
|
||||
MAX_TAG_LENGTH,
|
||||
Numeric,
|
||||
OUTCOME_TYPES,
|
||||
} from '../../common/contract'
|
||||
import { slugify } from '../../common/util/slugify'
|
||||
import { randomString } from '../../common/util/random'
|
||||
import { getNewContract } from '../../common/new-contract'
|
||||
|
||||
import { chargeUser } from './utils'
|
||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
||||
|
||||
import {
|
||||
FIXED_ANTE,
|
||||
getAnteBets,
|
||||
getCpmmInitialLiquidity,
|
||||
getFreeAnswerAnte,
|
||||
getNumericAnte,
|
||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
MINIMUM_ANTE,
|
||||
} from '../../common/antes'
|
||||
import { getNoneAnswer } from '../../common/answer'
|
||||
import { getNewContract } from '../../common/new-contract'
|
||||
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
|
||||
|
||||
export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
||||
const [creator, _privateUser] = await lookupUser(await parseCredentials(req))
|
||||
|
@ -35,6 +41,8 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
|||
initialProb,
|
||||
closeTime,
|
||||
tags,
|
||||
min,
|
||||
max,
|
||||
manaLimitPerUser,
|
||||
} = req.body.data || {}
|
||||
|
||||
|
@ -56,9 +64,23 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
|||
)
|
||||
|
||||
outcomeType = outcomeType ?? 'BINARY'
|
||||
if (!['BINARY', 'MULTI', 'FREE_RESPONSE'].includes(outcomeType))
|
||||
|
||||
if (!OUTCOME_TYPES.includes(outcomeType))
|
||||
throw new APIError(400, 'Invalid outcomeType')
|
||||
|
||||
if (
|
||||
outcomeType === 'NUMERIC' &&
|
||||
!(
|
||||
min !== undefined &&
|
||||
max !== undefined &&
|
||||
isFinite(min) &&
|
||||
isFinite(max) &&
|
||||
min < max &&
|
||||
max - min > 0.01
|
||||
)
|
||||
)
|
||||
throw new APIError(400, 'Invalid range')
|
||||
|
||||
if (
|
||||
outcomeType === 'BINARY' &&
|
||||
(!initialProb || initialProb < 1 || initialProb > 99)
|
||||
|
@ -109,6 +131,9 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
|||
ante,
|
||||
closeTime,
|
||||
tags ?? [],
|
||||
NUMERIC_BUCKET_COUNT,
|
||||
min ?? 0,
|
||||
max ?? 0,
|
||||
manaLimitPerUser ?? 0
|
||||
)
|
||||
|
||||
|
@ -167,6 +192,19 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
|||
contract as FullContract<DPM, FreeResponse>,
|
||||
anteBetDoc.id
|
||||
)
|
||||
await anteBetDoc.set(anteBet)
|
||||
} else if (outcomeType === 'NUMERIC') {
|
||||
const anteBetDoc = firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.doc()
|
||||
|
||||
const anteBet = getNumericAnte(
|
||||
creator,
|
||||
contract as FullContract<DPM, Numeric>,
|
||||
ante,
|
||||
anteBetDoc.id
|
||||
)
|
||||
|
||||
await anteBetDoc.set(anteBet)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import { Contract, FreeResponseContract } from '../../common/contract'
|
|||
import { DPM_CREATOR_FEE } from '../../common/fees'
|
||||
import { PrivateUser, User } from '../../common/user'
|
||||
import { formatMoney, formatPercent } from '../../common/util/format'
|
||||
import { getValueFromBucket } from '../../common/calculate-dpm'
|
||||
|
||||
import { sendTemplateEmail } from './send-email'
|
||||
import { getPrivateUser, getUser } from './utils'
|
||||
|
||||
|
@ -104,6 +106,12 @@ const toDisplayResolution = (
|
|||
if (resolution === 'MKT' && resolutions) return 'MULTI'
|
||||
if (resolution === 'CANCEL') return 'N/A'
|
||||
|
||||
if (contract.outcomeType === 'NUMERIC' && contract.mechanism === 'dpm-2')
|
||||
return (
|
||||
contract.resolutionValue?.toString() ??
|
||||
getValueFromBucket(resolution, contract).toString()
|
||||
)
|
||||
|
||||
const answer = (contract as FreeResponseContract).answers?.find(
|
||||
(a) => a.id === resolution
|
||||
)
|
||||
|
|
|
@ -7,16 +7,16 @@ import {
|
|||
getNewBinaryCpmmBetInfo,
|
||||
getNewBinaryDpmBetInfo,
|
||||
getNewMultiBetInfo,
|
||||
getNumericBetsInfo,
|
||||
} from '../../common/new-bet'
|
||||
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { redeemShares } from './redeem-shares'
|
||||
import { Fees } from '../../common/fees'
|
||||
import { hasUserHitManaLimit } from '../../common/calculate'
|
||||
|
||||
export const placeBet = newEndpoint(['POST'], async (req, _res) => {
|
||||
const [bettor, _privateUser] = await lookupUser(await parseCredentials(req))
|
||||
const { amount, outcome, contractId } = req.body.data || {}
|
||||
const { amount, outcome, contractId, value } = req.body.data || {}
|
||||
|
||||
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
||||
throw new APIError(400, 'Invalid amount')
|
||||
|
@ -24,6 +24,9 @@ export const placeBet = newEndpoint(['POST'], async (req, _res) => {
|
|||
if (outcome !== 'YES' && outcome !== 'NO' && isNaN(+outcome))
|
||||
throw new APIError(400, 'Invalid outcome')
|
||||
|
||||
if (value !== undefined && !isFinite(value))
|
||||
throw new APIError(400, 'Invalid value')
|
||||
|
||||
// run as transaction to prevent race conditions
|
||||
return await firestore
|
||||
.runTransaction(async (transaction) => {
|
||||
|
@ -55,13 +58,6 @@ export const placeBet = newEndpoint(['POST'], async (req, _res) => {
|
|||
contractDoc.collection('answers').doc(outcome)
|
||||
)
|
||||
if (!answerSnap.exists) throw new APIError(400, 'Invalid contract')
|
||||
|
||||
const { status, message } = hasUserHitManaLimit(
|
||||
contract,
|
||||
yourBets,
|
||||
amount
|
||||
)
|
||||
if (status === 'error') throw new APIError(400, message)
|
||||
}
|
||||
|
||||
const newBetDoc = firestore
|
||||
|
@ -96,6 +92,15 @@ export const placeBet = newEndpoint(['POST'], async (req, _res) => {
|
|||
loanAmount,
|
||||
newBetDoc.id
|
||||
) as any)
|
||||
: outcomeType === 'NUMERIC' && mechanism === 'dpm-2'
|
||||
? getNumericBetsInfo(
|
||||
user,
|
||||
value,
|
||||
outcome,
|
||||
amount,
|
||||
contract,
|
||||
newBetDoc.id
|
||||
)
|
||||
: getNewMultiBetInfo(
|
||||
user,
|
||||
outcome,
|
||||
|
|
|
@ -22,6 +22,7 @@ export const resolveMarket = functions
|
|||
async (
|
||||
data: {
|
||||
outcome: string
|
||||
value?: number
|
||||
contractId: string
|
||||
probabilityInt?: number
|
||||
resolutions?: { [outcome: string]: number }
|
||||
|
@ -31,7 +32,7 @@ export const resolveMarket = functions
|
|||
const userId = context?.auth?.uid
|
||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||
|
||||
const { outcome, contractId, probabilityInt, resolutions } = data
|
||||
const { outcome, contractId, probabilityInt, resolutions, value } = data
|
||||
|
||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||
const contractSnap = await contractDoc.get()
|
||||
|
@ -50,10 +51,16 @@ export const resolveMarket = functions
|
|||
outcome !== 'CANCEL'
|
||||
)
|
||||
return { status: 'error', message: 'Invalid outcome' }
|
||||
} else if (outcomeType === 'NUMERIC') {
|
||||
if (isNaN(+outcome) && outcome !== 'CANCEL')
|
||||
return { status: 'error', message: 'Invalid outcome' }
|
||||
} else {
|
||||
return { status: 'error', message: 'Invalid contract outcomeType' }
|
||||
}
|
||||
|
||||
if (value !== undefined && !isFinite(value))
|
||||
return { status: 'error', message: 'Invalid value' }
|
||||
|
||||
if (
|
||||
outcomeType === 'BINARY' &&
|
||||
probabilityInt !== undefined &&
|
||||
|
@ -108,6 +115,7 @@ export const resolveMarket = functions
|
|||
removeUndefinedProps({
|
||||
isResolved: true,
|
||||
resolution: outcome,
|
||||
resolutionValue: value,
|
||||
resolutionTime,
|
||||
closeTime: newCloseTime,
|
||||
resolutionProbability,
|
||||
|
|
|
@ -39,6 +39,7 @@ import {
|
|||
} from 'common/calculate'
|
||||
import { useTimeSinceFirstRender } from 'web/hooks/use-time-since-first-render'
|
||||
import { trackLatency } from 'web/lib/firebase/tracking'
|
||||
import { NumericContract } from 'common/contract'
|
||||
|
||||
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
||||
type BetFilter = 'open' | 'closed' | 'resolved' | 'all'
|
||||
|
@ -227,6 +228,8 @@ function MyContractBets(props: {
|
|||
const { bets, contract, metric } = props
|
||||
const { resolution, outcomeType } = contract
|
||||
|
||||
const resolutionValue = (contract as NumericContract).resolutionValue
|
||||
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
@ -272,6 +275,7 @@ function MyContractBets(props: {
|
|||
Resolved{' '}
|
||||
<OutcomeLabel
|
||||
outcome={resolution}
|
||||
value={resolutionValue}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
|
@ -430,8 +434,9 @@ export function ContractBetsTable(props: {
|
|||
(bet) => bet.loanAmount ?? 0
|
||||
)
|
||||
|
||||
const { isResolved, mechanism } = contract
|
||||
const { isResolved, mechanism, outcomeType } = contract
|
||||
const isCPMM = mechanism === 'cpmm-1'
|
||||
const isNumeric = outcomeType === 'NUMERIC'
|
||||
|
||||
return (
|
||||
<div className={clsx('overflow-x-auto', className)}>
|
||||
|
@ -461,7 +466,9 @@ export function ContractBetsTable(props: {
|
|||
{isCPMM && <th>Type</th>}
|
||||
<th>Outcome</th>
|
||||
<th>Amount</th>
|
||||
{!isCPMM && <th>{isResolved ? <>Payout</> : <>Sale price</>}</th>}
|
||||
{!isCPMM && !isNumeric && (
|
||||
<th>{isResolved ? <>Payout</> : <>Sale price</>}</th>
|
||||
)}
|
||||
{!isCPMM && !isResolved && <th>Payout if chosen</th>}
|
||||
<th>Shares</th>
|
||||
<th>Probability</th>
|
||||
|
@ -496,11 +503,12 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
|||
isAnte,
|
||||
} = bet
|
||||
|
||||
const { isResolved, closeTime, mechanism } = contract
|
||||
const { isResolved, closeTime, mechanism, outcomeType } = contract
|
||||
|
||||
const isClosed = closeTime && Date.now() > closeTime
|
||||
|
||||
const isCPMM = mechanism === 'cpmm-1'
|
||||
const isNumeric = outcomeType === 'NUMERIC'
|
||||
|
||||
const saleAmount = saleBet?.sale?.amount
|
||||
|
||||
|
@ -517,31 +525,35 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
|||
)
|
||||
|
||||
const payoutIfChosenDisplay =
|
||||
bet.outcome === '0' && bet.isAnte
|
||||
bet.isAnte && outcomeType === 'FREE_RESPONSE' && bet.outcome === '0'
|
||||
? 'N/A'
|
||||
: formatMoney(calculatePayout(contract, bet, bet.outcome))
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className="text-neutral">
|
||||
{!isCPMM && !isResolved && !isClosed && !isSold && !isAnte && (
|
||||
<SellButton contract={contract} bet={bet} />
|
||||
)}
|
||||
{!isCPMM &&
|
||||
!isResolved &&
|
||||
!isClosed &&
|
||||
!isSold &&
|
||||
!isAnte &&
|
||||
!isNumeric && <SellButton contract={contract} bet={bet} />}
|
||||
</td>
|
||||
{isCPMM && <td>{shares >= 0 ? 'BUY' : 'SELL'}</td>}
|
||||
<td>
|
||||
{outcome === '0' ? (
|
||||
{bet.isAnte ? (
|
||||
'ANTE'
|
||||
) : (
|
||||
<OutcomeLabel
|
||||
outcome={outcome}
|
||||
value={(bet as any).value}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>{formatMoney(Math.abs(amount))}</td>
|
||||
{!isCPMM && <td>{saleDisplay}</td>}
|
||||
{!isCPMM && !isNumeric && <td>{saleDisplay}</td>}
|
||||
{!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>}
|
||||
<td>{formatWithCommas(Math.abs(shares))}</td>
|
||||
<td>
|
||||
|
|
43
web/components/bucket-input.tsx
Normal file
43
web/components/bucket-input.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import _ from 'lodash'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { NumericContract } from 'common/contract'
|
||||
import { getMappedBucket } from 'common/calculate-dpm'
|
||||
|
||||
import { NumberInput } from './number-input'
|
||||
|
||||
export function BucketInput(props: {
|
||||
contract: NumericContract
|
||||
isSubmitting?: boolean
|
||||
onBucketChange: (value?: number, bucket?: string) => void
|
||||
}) {
|
||||
const { contract, isSubmitting, onBucketChange } = props
|
||||
|
||||
const [numberString, setNumberString] = useState('')
|
||||
|
||||
const onChange = (s: string) => {
|
||||
setNumberString(s)
|
||||
|
||||
const value = parseFloat(s)
|
||||
|
||||
if (!isFinite(value)) {
|
||||
onBucketChange(undefined, undefined)
|
||||
return
|
||||
}
|
||||
|
||||
const bucket = getMappedBucket(value, contract)
|
||||
|
||||
onBucketChange(value, bucket)
|
||||
}
|
||||
|
||||
return (
|
||||
<NumberInput
|
||||
inputClassName="w-full max-w-none"
|
||||
onChange={onChange}
|
||||
error={undefined}
|
||||
disabled={isSubmitting}
|
||||
numberString={numberString}
|
||||
label="Value"
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -16,6 +16,7 @@ import {
|
|||
FreeResponse,
|
||||
FreeResponseContract,
|
||||
FullContract,
|
||||
NumericContract,
|
||||
} from 'common/contract'
|
||||
import {
|
||||
AnswerLabel,
|
||||
|
@ -25,6 +26,7 @@ import {
|
|||
} from '../outcome-label'
|
||||
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
|
||||
import { AbbrContractDetails } from './contract-details'
|
||||
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
|
||||
|
||||
// Return a number from 0 to 1 for this contract
|
||||
// Resolved contracts are set to 1, for coloring purposes (even if NO)
|
||||
|
@ -105,6 +107,13 @@ export function ContractCard(props: {
|
|||
contract={contract}
|
||||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'NUMERIC' && (
|
||||
<NumericResolutionOrExpectation
|
||||
className="items-center"
|
||||
contract={contract as NumericContract}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
{outcomeType === 'FREE_RESPONSE' && (
|
||||
|
@ -214,3 +223,32 @@ export function FreeResponseResolutionOrChance(props: {
|
|||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export function NumericResolutionOrExpectation(props: {
|
||||
contract: NumericContract
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, className } = props
|
||||
const { resolution } = contract
|
||||
|
||||
const resolutionValue =
|
||||
contract.resolutionValue ?? getValueFromBucket(resolution ?? '', contract)
|
||||
|
||||
return (
|
||||
<Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}>
|
||||
{resolution ? (
|
||||
<>
|
||||
<div className={clsx('text-base text-gray-500')}>Resolved</div>
|
||||
<div className="text-blue-400">{resolutionValue}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-3xl text-blue-400">
|
||||
{getExpectedValue(contract)}
|
||||
</div>
|
||||
<div className="text-base text-blue-400">expected</div>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,18 +6,26 @@ import { useUser } from 'web/hooks/use-user'
|
|||
import { Row } from '../layout/row'
|
||||
import { Linkify } from '../linkify'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import {
|
||||
FreeResponseResolutionOrChance,
|
||||
BinaryResolutionOrChance,
|
||||
NumericResolutionOrExpectation,
|
||||
} from './contract-card'
|
||||
import { Bet } from 'common/bet'
|
||||
import { Comment } from 'common/comment'
|
||||
import BetRow from '../bet-row'
|
||||
import { AnswersGraph } from '../answers/answers-graph'
|
||||
import { DPM, FreeResponse, FullContract } from 'common/contract'
|
||||
import {
|
||||
DPM,
|
||||
FreeResponse,
|
||||
FullContract,
|
||||
NumericContract,
|
||||
} from 'common/contract'
|
||||
import { ContractDescription } from './contract-description'
|
||||
import { ContractDetails } from './contract-details'
|
||||
import { ShareMarket } from '../share-market'
|
||||
import { NumericGraph } from './numeric-graph'
|
||||
|
||||
export const ContractOverview = (props: {
|
||||
contract: Contract
|
||||
|
@ -47,6 +55,13 @@ export const ContractOverview = (props: {
|
|||
large
|
||||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'NUMERIC' && (
|
||||
<NumericResolutionOrExpectation
|
||||
contract={contract as NumericContract}
|
||||
className="hidden items-end xl:flex"
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
{isBinary ? (
|
||||
|
@ -65,28 +80,33 @@ export const ContractOverview = (props: {
|
|||
)
|
||||
)}
|
||||
|
||||
{outcomeType === 'NUMERIC' && (
|
||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||
<NumericResolutionOrExpectation
|
||||
contract={contract as NumericContract}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<ContractDetails
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
isCreator={isCreator}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
{isBinary ? (
|
||||
<ContractProbGraph contract={contract} bets={bets} />
|
||||
) : (
|
||||
{isBinary && <ContractProbGraph contract={contract} bets={bets} />}{' '}
|
||||
{outcomeType === 'FREE_RESPONSE' && (
|
||||
<AnswersGraph
|
||||
contract={contract as FullContract<DPM, FreeResponse>}
|
||||
bets={bets}
|
||||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'NUMERIC' && (
|
||||
<NumericGraph contract={contract as NumericContract} />
|
||||
)}
|
||||
{(contract.description || isCreator) && <Spacer h={6} />}
|
||||
|
||||
{isCreator && <ShareMarket className="px-2" contract={contract} />}
|
||||
|
||||
<ContractDescription
|
||||
className="px-2"
|
||||
contract={contract}
|
||||
|
|
|
@ -16,6 +16,7 @@ export function ContractTabs(props: {
|
|||
comments: Comment[]
|
||||
}) {
|
||||
const { contract, user, comments } = props
|
||||
const { outcomeType } = contract
|
||||
|
||||
const bets = useBets(contract.id) ?? props.bets
|
||||
// Decending creation time.
|
||||
|
@ -47,7 +48,7 @@ export function ContractTabs(props: {
|
|||
}
|
||||
betRowClassName="!mt-0 xl:hidden"
|
||||
/>
|
||||
{contract.outcomeType === 'FREE_RESPONSE' && (
|
||||
{outcomeType === 'FREE_RESPONSE' && (
|
||||
<Col className={'mt-8 flex w-full '}>
|
||||
<div className={'text-md mt-8 mb-2 text-left'}>General Comments</div>
|
||||
<div className={'mb-4 w-full border-b border-gray-200'} />
|
||||
|
|
76
web/components/contract/numeric-graph.tsx
Normal file
76
web/components/contract/numeric-graph.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { DatumValue } from '@nivo/core'
|
||||
import { ResponsiveLine } from '@nivo/line'
|
||||
import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants'
|
||||
import _ from 'lodash'
|
||||
import { memo } from 'react'
|
||||
import { getDpmOutcomeProbabilities } from '../../../common/calculate-dpm'
|
||||
import { NumericContract } from '../../../common/contract'
|
||||
import { useWindowSize } from '../../hooks/use-window-size'
|
||||
|
||||
export const NumericGraph = memo(function NumericGraph(props: {
|
||||
contract: NumericContract
|
||||
height?: number
|
||||
}) {
|
||||
const { contract, height } = props
|
||||
const { totalShares, bucketCount, min, max } = contract
|
||||
|
||||
const bucketProbs = getDpmOutcomeProbabilities(totalShares)
|
||||
|
||||
const xs = _.range(bucketCount).map(
|
||||
(i) => min + ((max - min) * i) / bucketCount
|
||||
)
|
||||
const probs = _.range(bucketCount).map((i) => bucketProbs[`${i}`] * 100)
|
||||
const points = probs.map((prob, i) => ({ x: xs[i], y: prob }))
|
||||
const maxProb = Math.max(...probs)
|
||||
const data = [{ id: 'Probability', data: points, color: NUMERIC_GRAPH_COLOR }]
|
||||
|
||||
const yTickValues = [
|
||||
0,
|
||||
0.25 * maxProb,
|
||||
0.5 & maxProb,
|
||||
0.75 * maxProb,
|
||||
maxProb,
|
||||
]
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
const numXTickValues = !width || width < 800 ? 2 : 5
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full overflow-hidden"
|
||||
style={{ height: height ?? (!width || width >= 800 ? 350 : 250) }}
|
||||
>
|
||||
<ResponsiveLine
|
||||
data={data}
|
||||
yScale={{ min: 0, max: maxProb, type: 'linear' }}
|
||||
yFormat={formatPercent}
|
||||
axisLeft={{
|
||||
tickValues: yTickValues,
|
||||
format: formatPercent,
|
||||
}}
|
||||
xScale={{
|
||||
type: 'linear',
|
||||
min: min,
|
||||
max: max,
|
||||
}}
|
||||
xFormat={(d) => `${Math.round(+d * 100) / 100}`}
|
||||
axisBottom={{
|
||||
tickValues: numXTickValues,
|
||||
format: (d) => `${Math.round(+d * 100) / 100}`,
|
||||
}}
|
||||
colors={{ datum: 'color' }}
|
||||
pointSize={0}
|
||||
enableSlices="x"
|
||||
enableGridX={!!width && width >= 800}
|
||||
enableArea
|
||||
margin={{ top: 20, right: 28, bottom: 22, left: 50 }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
function formatPercent(y: DatumValue) {
|
||||
const p = Math.round(+y * 100) / 100
|
||||
return `${p}%`
|
||||
}
|
|
@ -86,6 +86,7 @@ export function BetStatusText(props: {
|
|||
of{' '}
|
||||
<OutcomeLabel
|
||||
outcome={outcome}
|
||||
value={(bet as any).value}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
|
|
|
@ -161,7 +161,9 @@ export function FeedComment(props: {
|
|||
username={userUsername}
|
||||
name={userName}
|
||||
/>{' '}
|
||||
{!matchedBet && userPosition > 0 && (
|
||||
{!matchedBet &&
|
||||
userPosition > 0 &&
|
||||
contract.outcomeType !== 'NUMERIC' && (
|
||||
<>
|
||||
{'is '}
|
||||
<CommentStatus
|
||||
|
@ -179,6 +181,7 @@ export function FeedComment(props: {
|
|||
of{' '}
|
||||
<OutcomeLabel
|
||||
outcome={betOutcome ? betOutcome : ''}
|
||||
value={(matchedBet as any).value}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
|
@ -314,6 +317,8 @@ export function CommentInput(props: {
|
|||
|
||||
const shouldCollapseAfterClickOutside = false
|
||||
|
||||
const isNumeric = contract.outcomeType === 'NUMERIC'
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row className={'mb-2 flex w-full gap-2'}>
|
||||
|
@ -328,10 +333,15 @@ export function CommentInput(props: {
|
|||
contract={contract}
|
||||
bet={mostRecentCommentableBet}
|
||||
isSelf={true}
|
||||
hideOutcome={contract.outcomeType === 'FREE_RESPONSE'}
|
||||
hideOutcome={
|
||||
isNumeric || contract.outcomeType === 'FREE_RESPONSE'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!mostRecentCommentableBet && user && userPosition > 0 && (
|
||||
{!mostRecentCommentableBet &&
|
||||
user &&
|
||||
userPosition > 0 &&
|
||||
!isNumeric && (
|
||||
<>
|
||||
{"You're"}
|
||||
<CommentStatus
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
TruncatedComment,
|
||||
} from 'web/components/feed/feed-comments'
|
||||
import { FeedBet, FeedBetGroup } from 'web/components/feed/feed-bets'
|
||||
import { NumericContract } from 'common/contract'
|
||||
|
||||
export function FeedItems(props: {
|
||||
contract: Contract
|
||||
|
@ -215,8 +216,11 @@ function OutcomeIcon(props: { outcome?: string }) {
|
|||
function FeedResolve(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
const { creatorName, creatorUsername } = contract
|
||||
|
||||
const resolution = contract.resolution || 'CANCEL'
|
||||
|
||||
const resolutionValue = (contract as NumericContract).resolutionValue
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
|
@ -236,6 +240,7 @@ function FeedResolve(props: { contract: Contract }) {
|
|||
resolved this market to{' '}
|
||||
<OutcomeLabel
|
||||
outcome={resolution}
|
||||
value={resolutionValue}
|
||||
contract={contract}
|
||||
truncate="long"
|
||||
/>{' '}
|
||||
|
|
62
web/components/number-input.tsx
Normal file
62
web/components/number-input.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import clsx from 'clsx'
|
||||
import _ from 'lodash'
|
||||
|
||||
import { Col } from './layout/col'
|
||||
import { Spacer } from './layout/spacer'
|
||||
|
||||
export function NumberInput(props: {
|
||||
numberString: string
|
||||
onChange: (newNumberString: string) => void
|
||||
error: string | undefined
|
||||
label: string
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
inputClassName?: string
|
||||
// Needed to focus the amount input
|
||||
inputRef?: React.MutableRefObject<any>
|
||||
children?: any
|
||||
}) {
|
||||
const {
|
||||
numberString,
|
||||
onChange,
|
||||
error,
|
||||
label,
|
||||
disabled,
|
||||
className,
|
||||
inputClassName,
|
||||
inputRef,
|
||||
children,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Col className={className}>
|
||||
<label className="input-group">
|
||||
<span className="bg-gray-200 text-sm">{label}</span>
|
||||
<input
|
||||
className={clsx(
|
||||
'input input-bordered max-w-[200px] text-lg',
|
||||
error && 'input-error',
|
||||
inputClassName
|
||||
)}
|
||||
ref={inputRef}
|
||||
type="number"
|
||||
placeholder="0"
|
||||
maxLength={9}
|
||||
value={numberString}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(e.target.value.substring(0, 9))}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
{error && (
|
||||
<div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</Col>
|
||||
)
|
||||
}
|
207
web/components/numeric-bet-panel.tsx
Normal file
207
web/components/numeric-bet-panel.tsx
Normal file
|
@ -0,0 +1,207 @@
|
|||
import clsx from 'clsx'
|
||||
import { getNumericBetsInfo } from 'common/new-bet'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Bet } from '../../common/bet'
|
||||
import {
|
||||
calculatePayoutAfterCorrectBet,
|
||||
getOutcomeProbability,
|
||||
} from '../../common/calculate'
|
||||
import { NumericContract } from '../../common/contract'
|
||||
import { formatPercent, formatMoney } from '../../common/util/format'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import { placeBet } from '../lib/firebase/api-call'
|
||||
import { firebaseLogin, User } from '../lib/firebase/users'
|
||||
import { BuyAmountInput } from './amount-input'
|
||||
import { BucketInput } from './bucket-input'
|
||||
import { Col } from './layout/col'
|
||||
import { Row } from './layout/row'
|
||||
import { Spacer } from './layout/spacer'
|
||||
|
||||
export function NumericBetPanel(props: {
|
||||
contract: NumericContract
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, className } = props
|
||||
const user = useUser()
|
||||
|
||||
return (
|
||||
<Col className={clsx('rounded-md bg-white px-8 py-6', className)}>
|
||||
<div className="mb-6 text-2xl">Place your bet</div>
|
||||
|
||||
<NumericBuyPanel contract={contract} user={user} />
|
||||
|
||||
{user === null && (
|
||||
<button
|
||||
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
||||
onClick={firebaseLogin}
|
||||
>
|
||||
Sign up to trade!
|
||||
</button>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
function NumericBuyPanel(props: {
|
||||
contract: NumericContract
|
||||
user: User | null | undefined
|
||||
onBuySuccess?: () => void
|
||||
}) {
|
||||
const { contract, user, onBuySuccess } = props
|
||||
|
||||
const [bucketChoice, setBucketChoice] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [value, setValue] = useState<number | undefined>(undefined)
|
||||
|
||||
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
||||
|
||||
const [valueError, setValueError] = useState<string | undefined>()
|
||||
const [error, setError] = useState<string | undefined>()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [wasSubmitted, setWasSubmitted] = useState(false)
|
||||
|
||||
function onBetChange(newAmount: number | undefined) {
|
||||
setWasSubmitted(false)
|
||||
setBetAmount(newAmount)
|
||||
}
|
||||
|
||||
async function submitBet() {
|
||||
if (
|
||||
!user ||
|
||||
!betAmount ||
|
||||
bucketChoice === undefined ||
|
||||
value === undefined
|
||||
)
|
||||
return
|
||||
|
||||
setError(undefined)
|
||||
setIsSubmitting(true)
|
||||
|
||||
const result = await placeBet({
|
||||
amount: betAmount,
|
||||
outcome: bucketChoice,
|
||||
value,
|
||||
contractId: contract.id,
|
||||
}).then((r) => r.data as any)
|
||||
|
||||
console.log('placed bet. Result:', result)
|
||||
|
||||
if (result?.status === 'success') {
|
||||
setIsSubmitting(false)
|
||||
setWasSubmitted(true)
|
||||
setBetAmount(undefined)
|
||||
if (onBuySuccess) onBuySuccess()
|
||||
} else {
|
||||
setError(result?.message || 'Error placing bet')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const betDisabled = isSubmitting || !betAmount || !bucketChoice || error
|
||||
|
||||
const { newBet, newPool, newTotalShares, newTotalBets } = getNumericBetsInfo(
|
||||
{ id: 'dummy', balance: 0 } as User, // a little hackish
|
||||
value ?? 0,
|
||||
bucketChoice ?? 'NaN',
|
||||
betAmount ?? 0,
|
||||
contract,
|
||||
'dummy id'
|
||||
)
|
||||
|
||||
const { probAfter: outcomeProb, shares } = newBet
|
||||
|
||||
const initialProb = bucketChoice
|
||||
? getOutcomeProbability(contract, bucketChoice)
|
||||
: 0
|
||||
|
||||
const currentPayout =
|
||||
betAmount && bucketChoice
|
||||
? calculatePayoutAfterCorrectBet(
|
||||
{
|
||||
...contract,
|
||||
pool: newPool,
|
||||
totalShares: newTotalShares,
|
||||
totalBets: newTotalBets,
|
||||
},
|
||||
{
|
||||
outcome: bucketChoice,
|
||||
amount: betAmount,
|
||||
shares,
|
||||
} as Bet
|
||||
)
|
||||
: 0
|
||||
|
||||
const currentReturn =
|
||||
betAmount && bucketChoice ? (currentPayout - betAmount) / betAmount : 0
|
||||
const currentReturnPercent = formatPercent(currentReturn)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="my-3 text-left text-sm text-gray-500">
|
||||
Predicted value
|
||||
</div>
|
||||
|
||||
<BucketInput
|
||||
contract={contract}
|
||||
isSubmitting={isSubmitting}
|
||||
onBucketChange={(v, b) => (setValue(v), setBucketChoice(b))}
|
||||
/>
|
||||
|
||||
<div className="my-3 text-left text-sm text-gray-500">Bet amount</div>
|
||||
<BuyAmountInput
|
||||
inputClassName="w-full max-w-none"
|
||||
amount={betAmount}
|
||||
onChange={onBetChange}
|
||||
error={error}
|
||||
setError={setError}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
<Col className="mt-3 w-full gap-3">
|
||||
<Row className="items-center justify-between text-sm">
|
||||
<div className="text-gray-500">Probability</div>
|
||||
<Row>
|
||||
<div>{formatPercent(initialProb)}</div>
|
||||
<div className="mx-2">→</div>
|
||||
<div>{formatPercent(outcomeProb)}</div>
|
||||
</Row>
|
||||
</Row>
|
||||
|
||||
<Row className="items-center justify-between gap-2 text-sm">
|
||||
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
|
||||
<div>
|
||||
Estimated
|
||||
<br /> payout if correct
|
||||
</div>
|
||||
</Row>
|
||||
<Row className="flex-wrap items-end justify-end gap-2">
|
||||
<span className="whitespace-nowrap">
|
||||
{formatMoney(currentPayout)}
|
||||
</span>
|
||||
<span>(+{currentReturnPercent})</span>
|
||||
</Row>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<Spacer h={8} />
|
||||
|
||||
{user && (
|
||||
<button
|
||||
className={clsx(
|
||||
'btn flex-1',
|
||||
betDisabled ? 'btn-disabled' : 'btn-primary',
|
||||
isSubmitting ? 'loading' : ''
|
||||
)}
|
||||
onClick={betDisabled ? undefined : submitBet}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit bet'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{wasSubmitted && <div className="mt-4">Bet submitted!</div>}
|
||||
</>
|
||||
)
|
||||
}
|
101
web/components/numeric-resolution-panel.tsx
Normal file
101
web/components/numeric-resolution-panel.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
import clsx from 'clsx'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { Col } from './layout/col'
|
||||
import { User } from 'web/lib/firebase/users'
|
||||
import { NumberCancelSelector } from './yes-no-selector'
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { ResolveConfirmationButton } from './confirmation-button'
|
||||
import { resolveMarket } from 'web/lib/firebase/api-call'
|
||||
import { NumericContract } from 'common/contract'
|
||||
import { BucketInput } from './bucket-input'
|
||||
|
||||
export function NumericResolutionPanel(props: {
|
||||
creator: User
|
||||
contract: NumericContract
|
||||
className?: string
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// warm up cloud function
|
||||
resolveMarket({} as any).catch()
|
||||
}, [])
|
||||
|
||||
const { contract, className } = props
|
||||
|
||||
const [outcomeMode, setOutcomeMode] = useState<
|
||||
'NUMBER' | 'CANCEL' | undefined
|
||||
>()
|
||||
const [outcome, setOutcome] = useState<string | undefined>()
|
||||
const [value, setValue] = useState<number | undefined>()
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | undefined>(undefined)
|
||||
|
||||
const resolve = async () => {
|
||||
const finalOutcome = outcomeMode === 'NUMBER' ? outcome : 'CANCEL'
|
||||
if (outcomeMode === undefined || finalOutcome === undefined) return
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
const result = await resolveMarket({
|
||||
outcome: finalOutcome,
|
||||
value,
|
||||
contractId: contract.id,
|
||||
}).then((r) => r.data)
|
||||
|
||||
console.log('resolved', outcome, 'result:', result)
|
||||
|
||||
if (result?.status !== 'success') {
|
||||
setError(result?.message || 'Error resolving market')
|
||||
}
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
const submitButtonClass =
|
||||
outcomeMode === 'CANCEL'
|
||||
? 'bg-yellow-400 hover:bg-yellow-500'
|
||||
: outcome !== undefined
|
||||
? 'btn-primary'
|
||||
: 'btn-disabled'
|
||||
|
||||
return (
|
||||
<Col className={clsx('rounded-md bg-white px-8 py-6', className)}>
|
||||
<div className="mb-6 whitespace-nowrap text-2xl">Resolve market</div>
|
||||
|
||||
<div className="mb-3 text-sm text-gray-500">Outcome</div>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
<NumberCancelSelector selected={outcomeMode} onSelect={setOutcomeMode} />
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
{outcomeMode === 'NUMBER' && (
|
||||
<BucketInput
|
||||
contract={contract}
|
||||
isSubmitting={isSubmitting}
|
||||
onBucketChange={(v, o) => (setValue(v), setOutcome(o))}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{outcome === 'CANCEL' ? (
|
||||
<>All trades will be returned with no fees.</>
|
||||
) : (
|
||||
<>Resolving this market will immediately pay out traders.</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
{!!error && <div className="text-red-500">{error}</div>}
|
||||
|
||||
<ResolveConfirmationButton
|
||||
onResolve={resolve}
|
||||
isSubmitting={isSubmitting}
|
||||
openModalButtonClass={clsx('w-full mt-2', submitButtonClass)}
|
||||
submitButtonClass={submitButtonClass}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import clsx from 'clsx'
|
||||
import { Answer } from 'common/answer'
|
||||
import { getProbability } from 'common/calculate'
|
||||
import { getValueFromBucket } from 'common/calculate-dpm'
|
||||
import {
|
||||
Binary,
|
||||
Contract,
|
||||
|
@ -9,6 +10,7 @@ import {
|
|||
FreeResponse,
|
||||
FreeResponseContract,
|
||||
FullContract,
|
||||
NumericContract,
|
||||
} from 'common/contract'
|
||||
import { formatPercent } from 'common/util/format'
|
||||
import { ClientRender } from './client-render'
|
||||
|
@ -17,12 +19,20 @@ export function OutcomeLabel(props: {
|
|||
contract: Contract
|
||||
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string
|
||||
truncate: 'short' | 'long' | 'none'
|
||||
value?: number
|
||||
}) {
|
||||
const { outcome, contract, truncate } = props
|
||||
const { outcome, contract, truncate, value } = props
|
||||
|
||||
if (contract.outcomeType === 'BINARY')
|
||||
return <BinaryOutcomeLabel outcome={outcome as any} />
|
||||
|
||||
if (contract.outcomeType === 'NUMERIC')
|
||||
return (
|
||||
<span className="text-blue-500">
|
||||
{value ?? getValueFromBucket(outcome, contract as NumericContract)}
|
||||
</span>
|
||||
)
|
||||
|
||||
return (
|
||||
<FreeResponseOutcomeLabel
|
||||
contract={contract as FullContract<DPM, FreeResponse>}
|
||||
|
|
|
@ -195,6 +195,37 @@ export function BuyButton(props: { className?: string; onClick?: () => void }) {
|
|||
)
|
||||
}
|
||||
|
||||
export function NumberCancelSelector(props: {
|
||||
selected: 'NUMBER' | 'CANCEL' | undefined
|
||||
onSelect: (selected: 'NUMBER' | 'CANCEL') => void
|
||||
className?: string
|
||||
btnClassName?: string
|
||||
}) {
|
||||
const { selected, onSelect, className } = props
|
||||
|
||||
const btnClassName = clsx('px-6 flex-1', props.btnClassName)
|
||||
|
||||
return (
|
||||
<Col className={clsx('gap-2', className)}>
|
||||
<Button
|
||||
color={selected === 'NUMBER' ? 'green' : 'gray'}
|
||||
onClick={() => onSelect('NUMBER')}
|
||||
className={clsx('whitespace-nowrap', btnClassName)}
|
||||
>
|
||||
Choose value
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color={selected === 'CANCEL' ? 'yellow' : 'gray'}
|
||||
onClick={() => onSelect('CANCEL')}
|
||||
className={clsx(btnClassName, '')}
|
||||
>
|
||||
N/A
|
||||
</Button>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
function Button(props: {
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
|
|
|
@ -43,6 +43,7 @@ export const createAnswer = cloudFunction<
|
|||
export const resolveMarket = cloudFunction<
|
||||
{
|
||||
outcome: string
|
||||
value?: number
|
||||
contractId: string
|
||||
probabilityInt?: number
|
||||
resolutions?: { [outcome: string]: number }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { ArrowLeftIcon } from '@heroicons/react/outline'
|
||||
import _ from 'lodash'
|
||||
|
||||
import { useContractWithPreload } from 'web/hooks/use-contract'
|
||||
import { ContractOverview } from 'web/components/contract/contract-overview'
|
||||
|
@ -24,16 +25,23 @@ import Custom404 from '../404'
|
|||
import { AnswersPanel } from 'web/components/answers/answers-panel'
|
||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||
import { Leaderboard } from 'web/components/leaderboard'
|
||||
import _ from 'lodash'
|
||||
import { resolvedPayout } from 'common/calculate'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { useUserById } from 'web/hooks/use-users'
|
||||
import { ContractTabs } from 'web/components/contract/contract-tabs'
|
||||
import { FirstArgument } from 'common/util/types'
|
||||
import { DPM, FreeResponse, FullContract } from 'common/contract'
|
||||
import {
|
||||
BinaryContract,
|
||||
DPM,
|
||||
FreeResponse,
|
||||
FullContract,
|
||||
NumericContract,
|
||||
} from 'common/contract'
|
||||
import { contractTextDetails } from 'web/components/contract/contract-details'
|
||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||
import Confetti from 'react-confetti'
|
||||
import { NumericBetPanel } from '../../components/numeric-bet-panel'
|
||||
import { NumericResolutionPanel } from '../../components/numeric-resolution-panel'
|
||||
import { FeedComment } from 'web/components/feed/feed-comments'
|
||||
import { FeedBet } from 'web/components/feed/feed-bets'
|
||||
|
||||
|
@ -113,22 +121,40 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
|
|||
return <Custom404 />
|
||||
}
|
||||
|
||||
const { creatorId, isResolved, question, outcomeType, resolution } = contract
|
||||
const { creatorId, isResolved, question, outcomeType } = contract
|
||||
|
||||
const isCreator = user?.id === creatorId
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
const isNumeric = outcomeType === 'NUMERIC'
|
||||
const allowTrade = tradingAllowed(contract)
|
||||
const allowResolve = !isResolved && isCreator && !!user
|
||||
const hasSidePanel = isBinary && (allowTrade || allowResolve)
|
||||
const hasSidePanel = (isBinary || isNumeric) && (allowTrade || allowResolve)
|
||||
|
||||
const ogCardProps = getOpenGraphProps(contract)
|
||||
|
||||
const rightSidebar = hasSidePanel ? (
|
||||
<Col className="gap-4">
|
||||
{allowTrade && (
|
||||
{allowTrade &&
|
||||
(isNumeric ? (
|
||||
<NumericBetPanel
|
||||
className="hidden xl:flex"
|
||||
contract={contract as NumericContract}
|
||||
/>
|
||||
) : (
|
||||
<BetPanel className="hidden xl:flex" contract={contract} />
|
||||
)}
|
||||
{allowResolve && <ResolutionPanel creator={user} contract={contract} />}
|
||||
))}
|
||||
{allowResolve &&
|
||||
(isNumeric ? (
|
||||
<NumericResolutionPanel
|
||||
creator={user}
|
||||
contract={contract as NumericContract}
|
||||
/>
|
||||
) : (
|
||||
<ResolutionPanel
|
||||
creator={user}
|
||||
contract={contract as BinaryContract}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
) : null
|
||||
|
||||
|
@ -179,6 +205,13 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
|
|||
</>
|
||||
)}
|
||||
|
||||
{isNumeric && (
|
||||
<NumericBetPanel
|
||||
className="sm:hidden"
|
||||
contract={contract as NumericContract}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isResolved && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2">
|
||||
|
|
|
@ -66,6 +66,8 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
|
||||
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
||||
const [initialProb, setInitialProb] = useState(50)
|
||||
const [minString, setMinString] = useState('')
|
||||
const [maxString, setMaxString] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
|
||||
const [category, setCategory] = useState<string>('')
|
||||
|
@ -94,6 +96,9 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
|
||||
const balance = creator?.balance || 0
|
||||
|
||||
const min = minString ? parseFloat(minString) : undefined
|
||||
const max = maxString ? parseFloat(maxString) : undefined
|
||||
|
||||
const isValid =
|
||||
initialProb > 0 &&
|
||||
initialProb < 100 &&
|
||||
|
@ -104,7 +109,14 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
(ante <= balance || deservesDailyFreeMarket) &&
|
||||
// closeTime must be in the future
|
||||
closeTime &&
|
||||
closeTime > Date.now()
|
||||
closeTime > Date.now() &&
|
||||
(outcomeType !== 'NUMERIC' ||
|
||||
(min !== undefined &&
|
||||
max !== undefined &&
|
||||
isFinite(min) &&
|
||||
isFinite(max) &&
|
||||
min < max &&
|
||||
max - min > 0.01))
|
||||
|
||||
async function submit() {
|
||||
// TODO: Tell users why their contract is invalid
|
||||
|
@ -121,6 +133,8 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
ante,
|
||||
closeTime,
|
||||
tags: category ? [category] : undefined,
|
||||
min,
|
||||
max,
|
||||
})
|
||||
).then((r) => r.data || {})
|
||||
|
||||
|
@ -168,6 +182,17 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
/>
|
||||
<span className="label-text">Free response</span>
|
||||
</label>
|
||||
<label className="label cursor-pointer gap-2">
|
||||
<input
|
||||
className="radio"
|
||||
type="radio"
|
||||
name="opt"
|
||||
checked={outcomeType === 'NUMERIC'}
|
||||
value="NUMERIC"
|
||||
onChange={() => setOutcomeType('NUMERIC')}
|
||||
/>
|
||||
<span className="label-text">Numeric (experimental)</span>
|
||||
</label>
|
||||
</Row>
|
||||
<Spacer h={4} />
|
||||
|
||||
|
@ -184,6 +209,40 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
<div className="form-control mb-1 items-start">
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
DPM,
|
||||
FreeResponse,
|
||||
FullContract,
|
||||
NumericContract,
|
||||
} from 'common/contract'
|
||||
import { DOMAIN } from 'common/envs/constants'
|
||||
import { AnswersGraph } from 'web/components/answers/answers-graph'
|
||||
|
@ -12,6 +13,7 @@ import BetRow from 'web/components/bet-row'
|
|||
import {
|
||||
BinaryResolutionOrChance,
|
||||
FreeResponseResolutionOrChance,
|
||||
NumericResolutionOrExpectation,
|
||||
} from 'web/components/contract/contract-card'
|
||||
import { ContractDetails } from 'web/components/contract/contract-details'
|
||||
import { ContractProbGraph } from 'web/components/contract/contract-prob-graph'
|
||||
|
@ -129,6 +131,12 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
|||
truncate="long"
|
||||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'NUMERIC' && resolution && (
|
||||
<NumericResolutionOrExpectation
|
||||
contract={contract as NumericContract}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<Spacer h={2} />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import Router from 'next/router'
|
||||
|
||||
import { Page } from 'web/components/page'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
|
|
Loading…
Reference in New Issue
Block a user