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 { Bet, NumericBet } from './bet'
|
||||||
import { getDpmProbability } from './calculate-dpm'
|
import { getDpmProbability, getValueFromBucket } from './calculate-dpm'
|
||||||
import { Binary, CPMM, DPM, FreeResponse, FullContract } from './contract'
|
import {
|
||||||
|
Binary,
|
||||||
|
CPMM,
|
||||||
|
DPM,
|
||||||
|
FreeResponse,
|
||||||
|
FullContract,
|
||||||
|
Numeric,
|
||||||
|
} from './contract'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
import { LiquidityProvision } from './liquidity-provision'
|
import { LiquidityProvision } from './liquidity-provision'
|
||||||
import { noFees } from './fees'
|
import { noFees } from './fees'
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
export const FIXED_ANTE = 100
|
export const FIXED_ANTE = 100
|
||||||
|
|
||||||
|
@ -106,3 +114,42 @@ export function getFreeAnswerAnte(
|
||||||
|
|
||||||
return anteBet
|
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
|
createdTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NumericBet = Bet & {
|
||||||
|
value: number
|
||||||
|
allOutcomeShares: { [outcome: string]: number }
|
||||||
|
allBetAmounts: { [outcome: string]: number }
|
||||||
|
}
|
||||||
|
|
||||||
export const MAX_LOAN_PER_CONTRACT = 20
|
export const MAX_LOAN_PER_CONTRACT = 20
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
import * as _ from 'lodash'
|
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 { DPM_FEES } from './fees'
|
||||||
|
import { normpdf } from '../common/util/math'
|
||||||
|
import { addObjects } from './util/object'
|
||||||
|
|
||||||
export function getDpmProbability(totalShares: { [outcome: string]: number }) {
|
export function getDpmProbability(totalShares: { [outcome: string]: number }) {
|
||||||
// For binary contracts only.
|
// For binary contracts only.
|
||||||
|
@ -19,6 +29,91 @@ export function getDpmOutcomeProbability(
|
||||||
return shares ** 2 / squareSum
|
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(
|
export function getDpmOutcomeProbabilityAfterBet(
|
||||||
totalShares: {
|
totalShares: {
|
||||||
[outcome: string]: number
|
[outcome: string]: number
|
||||||
|
@ -63,6 +158,30 @@ export function calculateDpmShares(
|
||||||
return Math.sqrt(bet ** 2 + shares ** 2 + c) - shares
|
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(
|
export function calculateDpmRawShareValue(
|
||||||
totalShares: {
|
totalShares: {
|
||||||
[outcome: string]: number
|
[outcome: string]: number
|
||||||
|
@ -163,8 +282,15 @@ export function calculateStandardDpmPayout(
|
||||||
bet: Bet,
|
bet: Bet,
|
||||||
outcome: string
|
outcome: string
|
||||||
) {
|
) {
|
||||||
const { amount, outcome: betOutcome, shares } = bet
|
const { outcome: betOutcome } = bet
|
||||||
if (betOutcome !== outcome) return 0
|
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
|
const { totalShares, phantomShares, pool } = contract
|
||||||
if (!totalShares[outcome]) return 0
|
if (!totalShares[outcome]) return 0
|
||||||
|
@ -175,15 +301,20 @@ export function calculateStandardDpmPayout(
|
||||||
totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0)
|
totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0)
|
||||||
|
|
||||||
const winnings = (shares / total) * poolTotal
|
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(
|
export function calculateDpmPayoutAfterCorrectBet(
|
||||||
contract: FullContract<DPM, any>,
|
contract: FullContract<DPM, any>,
|
||||||
bet: Bet
|
bet: Bet
|
||||||
) {
|
) {
|
||||||
const { totalShares, pool, totalBets } = contract
|
const { totalShares, pool, totalBets, outcomeType } = contract
|
||||||
const { shares, amount, outcome } = bet
|
const { shares, amount, outcome } = bet
|
||||||
|
|
||||||
const prevShares = totalShares[outcome] ?? 0
|
const prevShares = totalShares[outcome] ?? 0
|
||||||
|
@ -204,19 +335,23 @@ export function calculateDpmPayoutAfterCorrectBet(
|
||||||
...totalBets,
|
...totalBets,
|
||||||
[outcome]: prevTotalBet + amount,
|
[outcome]: prevTotalBet + amount,
|
||||||
},
|
},
|
||||||
|
outcomeType:
|
||||||
|
outcomeType === 'NUMERIC'
|
||||||
|
? 'FREE_RESPONSE' // hack to show payout at particular bet point estimate
|
||||||
|
: outcomeType,
|
||||||
}
|
}
|
||||||
|
|
||||||
return calculateStandardDpmPayout(newContract, bet, outcome)
|
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')
|
if (contract.outcomeType === 'BINARY')
|
||||||
return calculateBinaryMktDpmPayout(contract, bet)
|
return calculateBinaryMktDpmPayout(contract, bet)
|
||||||
|
|
||||||
const { totalShares, pool, resolutions } = contract as FullContract<
|
const { totalShares, pool, resolutions, outcomeType } = contract
|
||||||
DPM,
|
|
||||||
FreeResponse
|
|
||||||
>
|
|
||||||
|
|
||||||
let probs: { [outcome: string]: number }
|
let probs: { [outcome: string]: number }
|
||||||
|
|
||||||
|
@ -239,10 +374,21 @@ function calculateMktDpmPayout(contract: FullContract<DPM, any>, bet: Bet) {
|
||||||
|
|
||||||
const { outcome, amount, shares } = bet
|
const { outcome, amount, shares } = bet
|
||||||
|
|
||||||
const totalPool = _.sum(Object.values(pool))
|
const poolFrac =
|
||||||
const poolFrac = (probs[outcome] * shares) / weightedShareTotal
|
outcomeType === 'NUMERIC'
|
||||||
const winnings = poolFrac * totalPool
|
? _.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)
|
return deductDpmFees(amount, winnings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -161,7 +161,6 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invested: Math.max(0, currentInvested),
|
invested: Math.max(0, currentInvested),
|
||||||
currentInvested,
|
|
||||||
payout,
|
payout,
|
||||||
netPayout,
|
netPayout,
|
||||||
profit,
|
profit,
|
||||||
|
@ -190,29 +189,3 @@ export function getTopAnswer(contract: FreeResponseContract) {
|
||||||
)
|
)
|
||||||
return top?.answer
|
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 { Answer } from './answer'
|
||||||
import { Fees } from './fees'
|
import { Fees } from './fees'
|
||||||
|
|
||||||
export type FullContract<
|
export type FullContract<
|
||||||
M extends DPM | CPMM,
|
M extends DPM | CPMM,
|
||||||
T extends Binary | Multi | FreeResponse
|
T extends Binary | Multi | FreeResponse | Numeric
|
||||||
> = {
|
> = {
|
||||||
id: string
|
id: string
|
||||||
slug: string // auto-generated; must be unique
|
slug: string // auto-generated; must be unique
|
||||||
|
@ -11,7 +12,7 @@ export type FullContract<
|
||||||
creatorId: string
|
creatorId: string
|
||||||
creatorName: string
|
creatorName: string
|
||||||
creatorUsername: string
|
creatorUsername: string
|
||||||
creatorAvatarUrl?: string // Start requiring after 2022-03-01
|
creatorAvatarUrl?: string
|
||||||
|
|
||||||
question: string
|
question: string
|
||||||
description: string // More info about what the contract is about
|
description: string // More info about what the contract is about
|
||||||
|
@ -41,9 +42,13 @@ export type FullContract<
|
||||||
} & M &
|
} & M &
|
||||||
T
|
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 BinaryContract = FullContract<DPM | CPMM, Binary>
|
||||||
export type FreeResponseContract = FullContract<DPM | CPMM, FreeResponse>
|
export type FreeResponseContract = FullContract<DPM | CPMM, FreeResponse>
|
||||||
|
export type NumericContract = FullContract<DPM, Numeric>
|
||||||
|
|
||||||
export type DPM = {
|
export type DPM = {
|
||||||
mechanism: 'dpm-2'
|
mechanism: 'dpm-2'
|
||||||
|
@ -83,7 +88,17 @@ export type FreeResponse = {
|
||||||
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
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_QUESTION_LENGTH = 480
|
||||||
export const MAX_DESCRIPTION_LENGTH = 10000
|
export const MAX_DESCRIPTION_LENGTH = 10000
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { Bet, MAX_LOAN_PER_CONTRACT } from './bet'
|
import { Bet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
|
||||||
import {
|
import {
|
||||||
calculateDpmShares,
|
calculateDpmShares,
|
||||||
getDpmProbability,
|
getDpmProbability,
|
||||||
getDpmOutcomeProbability,
|
getDpmOutcomeProbability,
|
||||||
|
getNumericBets,
|
||||||
|
calculateNumericDpmShares,
|
||||||
} from './calculate-dpm'
|
} from './calculate-dpm'
|
||||||
import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm'
|
import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm'
|
||||||
import {
|
import {
|
||||||
|
@ -14,9 +16,12 @@ import {
|
||||||
FreeResponse,
|
FreeResponse,
|
||||||
FullContract,
|
FullContract,
|
||||||
Multi,
|
Multi,
|
||||||
|
NumericContract,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
import { noFees } from './fees'
|
import { noFees } from './fees'
|
||||||
|
import { addObjects } from './util/object'
|
||||||
|
import { NUMERIC_FIXED_VAR } from './numeric-constants'
|
||||||
|
|
||||||
export const getNewBinaryCpmmBetInfo = (
|
export const getNewBinaryCpmmBetInfo = (
|
||||||
user: User,
|
user: User,
|
||||||
|
@ -154,6 +159,55 @@ export const getNewMultiBetInfo = (
|
||||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
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) => {
|
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
|
||||||
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
|
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
const prevLoanAmount = _.sumBy(openBets, (bet) => bet.loanAmount ?? 0)
|
const prevLoanAmount = _.sumBy(openBets, (bet) => bet.loanAmount ?? 0)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { PHANTOM_ANTE } from './antes'
|
import { PHANTOM_ANTE } from './antes'
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
|
@ -5,6 +7,7 @@ import {
|
||||||
CPMM,
|
CPMM,
|
||||||
DPM,
|
DPM,
|
||||||
FreeResponse,
|
FreeResponse,
|
||||||
|
Numeric,
|
||||||
outcomeType,
|
outcomeType,
|
||||||
} from './contract'
|
} from './contract'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
|
@ -23,6 +26,11 @@ export function getNewContract(
|
||||||
ante: number,
|
ante: number,
|
||||||
closeTime: number,
|
closeTime: number,
|
||||||
extraTags: string[],
|
extraTags: string[],
|
||||||
|
|
||||||
|
// used for numeric markets
|
||||||
|
bucketCount: number,
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
manaLimitPerUser: number
|
manaLimitPerUser: number
|
||||||
) {
|
) {
|
||||||
const tags = parseTags(
|
const tags = parseTags(
|
||||||
|
@ -33,6 +41,8 @@ export function getNewContract(
|
||||||
const propsByOutcomeType =
|
const propsByOutcomeType =
|
||||||
outcomeType === 'BINARY'
|
outcomeType === 'BINARY'
|
||||||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
||||||
|
: outcomeType === 'NUMERIC'
|
||||||
|
? getNumericProps(ante, bucketCount, min, max)
|
||||||
: getFreeAnswerProps(ante)
|
: getFreeAnswerProps(ante)
|
||||||
|
|
||||||
const contract: Contract = removeUndefinedProps({
|
const contract: Contract = removeUndefinedProps({
|
||||||
|
@ -115,6 +125,37 @@ const getFreeAnswerProps = (ante: number) => {
|
||||||
return system
|
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 = (
|
const getMultiProps = (
|
||||||
outcomes: string[],
|
outcomes: string[],
|
||||||
initialProbs: number[],
|
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 * as _ from 'lodash'
|
||||||
|
|
||||||
import { Bet } from './bet'
|
import { Bet, NumericBet } from './bet'
|
||||||
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
||||||
import { DPM, FreeResponse, FullContract, Multi } from './contract'
|
import { DPM, FreeResponse, FullContract, Multi } from './contract'
|
||||||
import {
|
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 = (
|
export const getDpmMktPayouts = (
|
||||||
contract: FullContract<DPM, any>,
|
contract: FullContract<DPM, any>,
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { Bet } from './bet'
|
import { Bet, NumericBet } from './bet'
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
Contract,
|
Contract,
|
||||||
|
@ -16,6 +16,7 @@ import {
|
||||||
getDpmCancelPayouts,
|
getDpmCancelPayouts,
|
||||||
getDpmMktPayouts,
|
getDpmMktPayouts,
|
||||||
getDpmStandardPayouts,
|
getDpmStandardPayouts,
|
||||||
|
getNumericDpmPayouts,
|
||||||
getPayoutsMultiOutcome,
|
getPayoutsMultiOutcome,
|
||||||
} from './payouts-dpm'
|
} from './payouts-dpm'
|
||||||
import {
|
import {
|
||||||
|
@ -131,6 +132,9 @@ export const getDpmPayouts = (
|
||||||
return getDpmCancelPayouts(contract, openBets)
|
return getDpmCancelPayouts(contract, openBets)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
if (contract.outcomeType === 'NUMERIC')
|
||||||
|
return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[])
|
||||||
|
|
||||||
// Outcome is a free response answer id.
|
// Outcome is a free response answer id.
|
||||||
return getDpmStandardPayouts(outcome, contract, openBets)
|
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)
|
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 { getContract, getValues } from './utils'
|
||||||
import { sendNewAnswerEmail } from './emails'
|
import { sendNewAnswerEmail } from './emails'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { hasUserHitManaLimit } from '../../common/calculate'
|
|
||||||
|
|
||||||
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
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 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>(
|
const [lastAnswer] = await getValues<Answer>(
|
||||||
firestore
|
firestore
|
||||||
.collection(`contracts/${contractId}/answers`)
|
.collection(`contracts/${contractId}/answers`)
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { chargeUser } from './utils'
|
|
||||||
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
Contract,
|
Contract,
|
||||||
|
@ -12,19 +10,27 @@ import {
|
||||||
MAX_DESCRIPTION_LENGTH,
|
MAX_DESCRIPTION_LENGTH,
|
||||||
MAX_QUESTION_LENGTH,
|
MAX_QUESTION_LENGTH,
|
||||||
MAX_TAG_LENGTH,
|
MAX_TAG_LENGTH,
|
||||||
|
Numeric,
|
||||||
|
OUTCOME_TYPES,
|
||||||
} from '../../common/contract'
|
} from '../../common/contract'
|
||||||
import { slugify } from '../../common/util/slugify'
|
import { slugify } from '../../common/util/slugify'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
import { getNewContract } from '../../common/new-contract'
|
|
||||||
|
import { chargeUser } from './utils'
|
||||||
|
import { APIError, newEndpoint, parseCredentials, lookupUser } from './api'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FIXED_ANTE,
|
FIXED_ANTE,
|
||||||
getAnteBets,
|
getAnteBets,
|
||||||
getCpmmInitialLiquidity,
|
getCpmmInitialLiquidity,
|
||||||
getFreeAnswerAnte,
|
getFreeAnswerAnte,
|
||||||
|
getNumericAnte,
|
||||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
MINIMUM_ANTE,
|
MINIMUM_ANTE,
|
||||||
} from '../../common/antes'
|
} from '../../common/antes'
|
||||||
import { getNoneAnswer } from '../../common/answer'
|
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) => {
|
export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
||||||
const [creator, _privateUser] = await lookupUser(await parseCredentials(req))
|
const [creator, _privateUser] = await lookupUser(await parseCredentials(req))
|
||||||
|
@ -35,6 +41,8 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
||||||
initialProb,
|
initialProb,
|
||||||
closeTime,
|
closeTime,
|
||||||
tags,
|
tags,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
manaLimitPerUser,
|
manaLimitPerUser,
|
||||||
} = req.body.data || {}
|
} = req.body.data || {}
|
||||||
|
|
||||||
|
@ -56,9 +64,23 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
outcomeType = outcomeType ?? 'BINARY'
|
outcomeType = outcomeType ?? 'BINARY'
|
||||||
if (!['BINARY', 'MULTI', 'FREE_RESPONSE'].includes(outcomeType))
|
|
||||||
|
if (!OUTCOME_TYPES.includes(outcomeType))
|
||||||
throw new APIError(400, 'Invalid 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 (
|
if (
|
||||||
outcomeType === 'BINARY' &&
|
outcomeType === 'BINARY' &&
|
||||||
(!initialProb || initialProb < 1 || initialProb > 99)
|
(!initialProb || initialProb < 1 || initialProb > 99)
|
||||||
|
@ -109,6 +131,9 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
||||||
ante,
|
ante,
|
||||||
closeTime,
|
closeTime,
|
||||||
tags ?? [],
|
tags ?? [],
|
||||||
|
NUMERIC_BUCKET_COUNT,
|
||||||
|
min ?? 0,
|
||||||
|
max ?? 0,
|
||||||
manaLimitPerUser ?? 0
|
manaLimitPerUser ?? 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -167,6 +192,19 @@ export const createContract = newEndpoint(['POST'], async (req, _res) => {
|
||||||
contract as FullContract<DPM, FreeResponse>,
|
contract as FullContract<DPM, FreeResponse>,
|
||||||
anteBetDoc.id
|
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)
|
await anteBetDoc.set(anteBet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { Contract, FreeResponseContract } from '../../common/contract'
|
||||||
import { DPM_CREATOR_FEE } from '../../common/fees'
|
import { DPM_CREATOR_FEE } from '../../common/fees'
|
||||||
import { PrivateUser, User } from '../../common/user'
|
import { PrivateUser, User } from '../../common/user'
|
||||||
import { formatMoney, formatPercent } from '../../common/util/format'
|
import { formatMoney, formatPercent } from '../../common/util/format'
|
||||||
|
import { getValueFromBucket } from '../../common/calculate-dpm'
|
||||||
|
|
||||||
import { sendTemplateEmail } from './send-email'
|
import { sendTemplateEmail } from './send-email'
|
||||||
import { getPrivateUser, getUser } from './utils'
|
import { getPrivateUser, getUser } from './utils'
|
||||||
|
|
||||||
|
@ -104,6 +106,12 @@ const toDisplayResolution = (
|
||||||
if (resolution === 'MKT' && resolutions) return 'MULTI'
|
if (resolution === 'MKT' && resolutions) return 'MULTI'
|
||||||
if (resolution === 'CANCEL') return 'N/A'
|
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(
|
const answer = (contract as FreeResponseContract).answers?.find(
|
||||||
(a) => a.id === resolution
|
(a) => a.id === resolution
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,16 +7,16 @@ import {
|
||||||
getNewBinaryCpmmBetInfo,
|
getNewBinaryCpmmBetInfo,
|
||||||
getNewBinaryDpmBetInfo,
|
getNewBinaryDpmBetInfo,
|
||||||
getNewMultiBetInfo,
|
getNewMultiBetInfo,
|
||||||
|
getNumericBetsInfo,
|
||||||
} from '../../common/new-bet'
|
} from '../../common/new-bet'
|
||||||
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { redeemShares } from './redeem-shares'
|
import { redeemShares } from './redeem-shares'
|
||||||
import { Fees } from '../../common/fees'
|
import { Fees } from '../../common/fees'
|
||||||
import { hasUserHitManaLimit } from '../../common/calculate'
|
|
||||||
|
|
||||||
export const placeBet = newEndpoint(['POST'], async (req, _res) => {
|
export const placeBet = newEndpoint(['POST'], async (req, _res) => {
|
||||||
const [bettor, _privateUser] = await lookupUser(await parseCredentials(req))
|
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))
|
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
||||||
throw new APIError(400, 'Invalid 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))
|
if (outcome !== 'YES' && outcome !== 'NO' && isNaN(+outcome))
|
||||||
throw new APIError(400, 'Invalid 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
|
// run as transaction to prevent race conditions
|
||||||
return await firestore
|
return await firestore
|
||||||
.runTransaction(async (transaction) => {
|
.runTransaction(async (transaction) => {
|
||||||
|
@ -55,13 +58,6 @@ export const placeBet = newEndpoint(['POST'], async (req, _res) => {
|
||||||
contractDoc.collection('answers').doc(outcome)
|
contractDoc.collection('answers').doc(outcome)
|
||||||
)
|
)
|
||||||
if (!answerSnap.exists) throw new APIError(400, 'Invalid contract')
|
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
|
const newBetDoc = firestore
|
||||||
|
@ -96,6 +92,15 @@ export const placeBet = newEndpoint(['POST'], async (req, _res) => {
|
||||||
loanAmount,
|
loanAmount,
|
||||||
newBetDoc.id
|
newBetDoc.id
|
||||||
) as any)
|
) as any)
|
||||||
|
: outcomeType === 'NUMERIC' && mechanism === 'dpm-2'
|
||||||
|
? getNumericBetsInfo(
|
||||||
|
user,
|
||||||
|
value,
|
||||||
|
outcome,
|
||||||
|
amount,
|
||||||
|
contract,
|
||||||
|
newBetDoc.id
|
||||||
|
)
|
||||||
: getNewMultiBetInfo(
|
: getNewMultiBetInfo(
|
||||||
user,
|
user,
|
||||||
outcome,
|
outcome,
|
||||||
|
|
|
@ -22,6 +22,7 @@ export const resolveMarket = functions
|
||||||
async (
|
async (
|
||||||
data: {
|
data: {
|
||||||
outcome: string
|
outcome: string
|
||||||
|
value?: number
|
||||||
contractId: string
|
contractId: string
|
||||||
probabilityInt?: number
|
probabilityInt?: number
|
||||||
resolutions?: { [outcome: string]: number }
|
resolutions?: { [outcome: string]: number }
|
||||||
|
@ -31,7 +32,7 @@ export const resolveMarket = functions
|
||||||
const userId = context?.auth?.uid
|
const userId = context?.auth?.uid
|
||||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
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 contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
const contractSnap = await contractDoc.get()
|
const contractSnap = await contractDoc.get()
|
||||||
|
@ -50,10 +51,16 @@ export const resolveMarket = functions
|
||||||
outcome !== 'CANCEL'
|
outcome !== 'CANCEL'
|
||||||
)
|
)
|
||||||
return { status: 'error', message: 'Invalid outcome' }
|
return { status: 'error', message: 'Invalid outcome' }
|
||||||
|
} else if (outcomeType === 'NUMERIC') {
|
||||||
|
if (isNaN(+outcome) && outcome !== 'CANCEL')
|
||||||
|
return { status: 'error', message: 'Invalid outcome' }
|
||||||
} else {
|
} else {
|
||||||
return { status: 'error', message: 'Invalid contract outcomeType' }
|
return { status: 'error', message: 'Invalid contract outcomeType' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value !== undefined && !isFinite(value))
|
||||||
|
return { status: 'error', message: 'Invalid value' }
|
||||||
|
|
||||||
if (
|
if (
|
||||||
outcomeType === 'BINARY' &&
|
outcomeType === 'BINARY' &&
|
||||||
probabilityInt !== undefined &&
|
probabilityInt !== undefined &&
|
||||||
|
@ -108,6 +115,7 @@ export const resolveMarket = functions
|
||||||
removeUndefinedProps({
|
removeUndefinedProps({
|
||||||
isResolved: true,
|
isResolved: true,
|
||||||
resolution: outcome,
|
resolution: outcome,
|
||||||
|
resolutionValue: value,
|
||||||
resolutionTime,
|
resolutionTime,
|
||||||
closeTime: newCloseTime,
|
closeTime: newCloseTime,
|
||||||
resolutionProbability,
|
resolutionProbability,
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
} from 'common/calculate'
|
} from 'common/calculate'
|
||||||
import { useTimeSinceFirstRender } from 'web/hooks/use-time-since-first-render'
|
import { useTimeSinceFirstRender } from 'web/hooks/use-time-since-first-render'
|
||||||
import { trackLatency } from 'web/lib/firebase/tracking'
|
import { trackLatency } from 'web/lib/firebase/tracking'
|
||||||
|
import { NumericContract } from 'common/contract'
|
||||||
|
|
||||||
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
||||||
type BetFilter = 'open' | 'closed' | 'resolved' | 'all'
|
type BetFilter = 'open' | 'closed' | 'resolved' | 'all'
|
||||||
|
@ -227,6 +228,8 @@ function MyContractBets(props: {
|
||||||
const { bets, contract, metric } = props
|
const { bets, contract, metric } = props
|
||||||
const { resolution, outcomeType } = contract
|
const { resolution, outcomeType } = contract
|
||||||
|
|
||||||
|
const resolutionValue = (contract as NumericContract).resolutionValue
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(true)
|
const [collapsed, setCollapsed] = useState(true)
|
||||||
|
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
@ -272,6 +275,7 @@ function MyContractBets(props: {
|
||||||
Resolved{' '}
|
Resolved{' '}
|
||||||
<OutcomeLabel
|
<OutcomeLabel
|
||||||
outcome={resolution}
|
outcome={resolution}
|
||||||
|
value={resolutionValue}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
truncate="short"
|
truncate="short"
|
||||||
/>
|
/>
|
||||||
|
@ -430,8 +434,9 @@ export function ContractBetsTable(props: {
|
||||||
(bet) => bet.loanAmount ?? 0
|
(bet) => bet.loanAmount ?? 0
|
||||||
)
|
)
|
||||||
|
|
||||||
const { isResolved, mechanism } = contract
|
const { isResolved, mechanism, outcomeType } = contract
|
||||||
const isCPMM = mechanism === 'cpmm-1'
|
const isCPMM = mechanism === 'cpmm-1'
|
||||||
|
const isNumeric = outcomeType === 'NUMERIC'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('overflow-x-auto', className)}>
|
<div className={clsx('overflow-x-auto', className)}>
|
||||||
|
@ -461,7 +466,9 @@ export function ContractBetsTable(props: {
|
||||||
{isCPMM && <th>Type</th>}
|
{isCPMM && <th>Type</th>}
|
||||||
<th>Outcome</th>
|
<th>Outcome</th>
|
||||||
<th>Amount</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>}
|
{!isCPMM && !isResolved && <th>Payout if chosen</th>}
|
||||||
<th>Shares</th>
|
<th>Shares</th>
|
||||||
<th>Probability</th>
|
<th>Probability</th>
|
||||||
|
@ -496,11 +503,12 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
isAnte,
|
isAnte,
|
||||||
} = bet
|
} = bet
|
||||||
|
|
||||||
const { isResolved, closeTime, mechanism } = contract
|
const { isResolved, closeTime, mechanism, outcomeType } = contract
|
||||||
|
|
||||||
const isClosed = closeTime && Date.now() > closeTime
|
const isClosed = closeTime && Date.now() > closeTime
|
||||||
|
|
||||||
const isCPMM = mechanism === 'cpmm-1'
|
const isCPMM = mechanism === 'cpmm-1'
|
||||||
|
const isNumeric = outcomeType === 'NUMERIC'
|
||||||
|
|
||||||
const saleAmount = saleBet?.sale?.amount
|
const saleAmount = saleBet?.sale?.amount
|
||||||
|
|
||||||
|
@ -517,31 +525,35 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const payoutIfChosenDisplay =
|
const payoutIfChosenDisplay =
|
||||||
bet.outcome === '0' && bet.isAnte
|
bet.isAnte && outcomeType === 'FREE_RESPONSE' && bet.outcome === '0'
|
||||||
? 'N/A'
|
? 'N/A'
|
||||||
: formatMoney(calculatePayout(contract, bet, bet.outcome))
|
: formatMoney(calculatePayout(contract, bet, bet.outcome))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="text-neutral">
|
<td className="text-neutral">
|
||||||
{!isCPMM && !isResolved && !isClosed && !isSold && !isAnte && (
|
{!isCPMM &&
|
||||||
<SellButton contract={contract} bet={bet} />
|
!isResolved &&
|
||||||
)}
|
!isClosed &&
|
||||||
|
!isSold &&
|
||||||
|
!isAnte &&
|
||||||
|
!isNumeric && <SellButton contract={contract} bet={bet} />}
|
||||||
</td>
|
</td>
|
||||||
{isCPMM && <td>{shares >= 0 ? 'BUY' : 'SELL'}</td>}
|
{isCPMM && <td>{shares >= 0 ? 'BUY' : 'SELL'}</td>}
|
||||||
<td>
|
<td>
|
||||||
{outcome === '0' ? (
|
{bet.isAnte ? (
|
||||||
'ANTE'
|
'ANTE'
|
||||||
) : (
|
) : (
|
||||||
<OutcomeLabel
|
<OutcomeLabel
|
||||||
outcome={outcome}
|
outcome={outcome}
|
||||||
|
value={(bet as any).value}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
truncate="short"
|
truncate="short"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{formatMoney(Math.abs(amount))}</td>
|
<td>{formatMoney(Math.abs(amount))}</td>
|
||||||
{!isCPMM && <td>{saleDisplay}</td>}
|
{!isCPMM && !isNumeric && <td>{saleDisplay}</td>}
|
||||||
{!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>}
|
{!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>}
|
||||||
<td>{formatWithCommas(Math.abs(shares))}</td>
|
<td>{formatWithCommas(Math.abs(shares))}</td>
|
||||||
<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,
|
FreeResponse,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
FullContract,
|
FullContract,
|
||||||
|
NumericContract,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
import {
|
import {
|
||||||
AnswerLabel,
|
AnswerLabel,
|
||||||
|
@ -25,6 +26,7 @@ import {
|
||||||
} from '../outcome-label'
|
} from '../outcome-label'
|
||||||
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
|
import { getOutcomeProbability, getTopAnswer } from 'common/calculate'
|
||||||
import { AbbrContractDetails } from './contract-details'
|
import { AbbrContractDetails } from './contract-details'
|
||||||
|
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
|
||||||
|
|
||||||
// Return a number from 0 to 1 for this contract
|
// Return a number from 0 to 1 for this contract
|
||||||
// Resolved contracts are set to 1, for coloring purposes (even if NO)
|
// Resolved contracts are set to 1, for coloring purposes (even if NO)
|
||||||
|
@ -105,6 +107,13 @@ export function ContractCard(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{outcomeType === 'NUMERIC' && (
|
||||||
|
<NumericResolutionOrExpectation
|
||||||
|
className="items-center"
|
||||||
|
contract={contract as NumericContract}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{outcomeType === 'FREE_RESPONSE' && (
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
|
@ -214,3 +223,32 @@ export function FreeResponseResolutionOrChance(props: {
|
||||||
</Col>
|
</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 { Row } from '../layout/row'
|
||||||
import { Linkify } from '../linkify'
|
import { Linkify } from '../linkify'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FreeResponseResolutionOrChance,
|
FreeResponseResolutionOrChance,
|
||||||
BinaryResolutionOrChance,
|
BinaryResolutionOrChance,
|
||||||
|
NumericResolutionOrExpectation,
|
||||||
} from './contract-card'
|
} from './contract-card'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Comment } from 'common/comment'
|
import { Comment } from 'common/comment'
|
||||||
import BetRow from '../bet-row'
|
import BetRow from '../bet-row'
|
||||||
import { AnswersGraph } from '../answers/answers-graph'
|
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 { ContractDescription } from './contract-description'
|
||||||
import { ContractDetails } from './contract-details'
|
import { ContractDetails } from './contract-details'
|
||||||
import { ShareMarket } from '../share-market'
|
import { ShareMarket } from '../share-market'
|
||||||
|
import { NumericGraph } from './numeric-graph'
|
||||||
|
|
||||||
export const ContractOverview = (props: {
|
export const ContractOverview = (props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -47,6 +55,13 @@ export const ContractOverview = (props: {
|
||||||
large
|
large
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{outcomeType === 'NUMERIC' && (
|
||||||
|
<NumericResolutionOrExpectation
|
||||||
|
contract={contract as NumericContract}
|
||||||
|
className="hidden items-end xl:flex"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{isBinary ? (
|
{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
|
<ContractDetails
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={bets}
|
bets={bets}
|
||||||
isCreator={isCreator}
|
isCreator={isCreator}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
{isBinary && <ContractProbGraph contract={contract} bets={bets} />}{' '}
|
||||||
{isBinary ? (
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
<ContractProbGraph contract={contract} bets={bets} />
|
|
||||||
) : (
|
|
||||||
<AnswersGraph
|
<AnswersGraph
|
||||||
contract={contract as FullContract<DPM, FreeResponse>}
|
contract={contract as FullContract<DPM, FreeResponse>}
|
||||||
bets={bets}
|
bets={bets}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{outcomeType === 'NUMERIC' && (
|
||||||
|
<NumericGraph contract={contract as NumericContract} />
|
||||||
|
)}
|
||||||
{(contract.description || isCreator) && <Spacer h={6} />}
|
{(contract.description || isCreator) && <Spacer h={6} />}
|
||||||
|
|
||||||
{isCreator && <ShareMarket className="px-2" contract={contract} />}
|
{isCreator && <ShareMarket className="px-2" contract={contract} />}
|
||||||
|
|
||||||
<ContractDescription
|
<ContractDescription
|
||||||
className="px-2"
|
className="px-2"
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export function ContractTabs(props: {
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
}) {
|
}) {
|
||||||
const { contract, user, comments } = props
|
const { contract, user, comments } = props
|
||||||
|
const { outcomeType } = contract
|
||||||
|
|
||||||
const bets = useBets(contract.id) ?? props.bets
|
const bets = useBets(contract.id) ?? props.bets
|
||||||
// Decending creation time.
|
// Decending creation time.
|
||||||
|
@ -47,7 +48,7 @@ export function ContractTabs(props: {
|
||||||
}
|
}
|
||||||
betRowClassName="!mt-0 xl:hidden"
|
betRowClassName="!mt-0 xl:hidden"
|
||||||
/>
|
/>
|
||||||
{contract.outcomeType === 'FREE_RESPONSE' && (
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
<Col className={'mt-8 flex w-full '}>
|
<Col className={'mt-8 flex w-full '}>
|
||||||
<div className={'text-md mt-8 mb-2 text-left'}>General Comments</div>
|
<div className={'text-md mt-8 mb-2 text-left'}>General Comments</div>
|
||||||
<div className={'mb-4 w-full border-b border-gray-200'} />
|
<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{' '}
|
of{' '}
|
||||||
<OutcomeLabel
|
<OutcomeLabel
|
||||||
outcome={outcome}
|
outcome={outcome}
|
||||||
|
value={(bet as any).value}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
truncate="short"
|
truncate="short"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -161,16 +161,18 @@ export function FeedComment(props: {
|
||||||
username={userUsername}
|
username={userUsername}
|
||||||
name={userName}
|
name={userName}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{!matchedBet && userPosition > 0 && (
|
{!matchedBet &&
|
||||||
<>
|
userPosition > 0 &&
|
||||||
{'is '}
|
contract.outcomeType !== 'NUMERIC' && (
|
||||||
<CommentStatus
|
<>
|
||||||
prob={probAtCreatedTime}
|
{'is '}
|
||||||
outcome={outcome}
|
<CommentStatus
|
||||||
contract={contract}
|
prob={probAtCreatedTime}
|
||||||
/>
|
outcome={outcome}
|
||||||
</>
|
contract={contract}
|
||||||
)}
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<>
|
<>
|
||||||
{bought} {money}
|
{bought} {money}
|
||||||
{contract.outcomeType !== 'FREE_RESPONSE' && betOutcome && (
|
{contract.outcomeType !== 'FREE_RESPONSE' && betOutcome && (
|
||||||
|
@ -179,6 +181,7 @@ export function FeedComment(props: {
|
||||||
of{' '}
|
of{' '}
|
||||||
<OutcomeLabel
|
<OutcomeLabel
|
||||||
outcome={betOutcome ? betOutcome : ''}
|
outcome={betOutcome ? betOutcome : ''}
|
||||||
|
value={(matchedBet as any).value}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
truncate="short"
|
truncate="short"
|
||||||
/>
|
/>
|
||||||
|
@ -314,6 +317,8 @@ export function CommentInput(props: {
|
||||||
|
|
||||||
const shouldCollapseAfterClickOutside = false
|
const shouldCollapseAfterClickOutside = false
|
||||||
|
|
||||||
|
const isNumeric = contract.outcomeType === 'NUMERIC'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row className={'mb-2 flex w-full gap-2'}>
|
<Row className={'mb-2 flex w-full gap-2'}>
|
||||||
|
@ -328,23 +333,28 @@ export function CommentInput(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bet={mostRecentCommentableBet}
|
bet={mostRecentCommentableBet}
|
||||||
isSelf={true}
|
isSelf={true}
|
||||||
hideOutcome={contract.outcomeType === 'FREE_RESPONSE'}
|
hideOutcome={
|
||||||
|
isNumeric || contract.outcomeType === 'FREE_RESPONSE'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!mostRecentCommentableBet && user && userPosition > 0 && (
|
{!mostRecentCommentableBet &&
|
||||||
<>
|
user &&
|
||||||
{"You're"}
|
userPosition > 0 &&
|
||||||
<CommentStatus
|
!isNumeric && (
|
||||||
outcome={outcome}
|
<>
|
||||||
contract={contract}
|
{"You're"}
|
||||||
prob={
|
<CommentStatus
|
||||||
contract.outcomeType === 'BINARY'
|
outcome={outcome}
|
||||||
? getProbability(contract)
|
contract={contract}
|
||||||
: undefined
|
prob={
|
||||||
}
|
contract.outcomeType === 'BINARY'
|
||||||
/>
|
? getProbability(contract)
|
||||||
</>
|
: undefined
|
||||||
)}
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Row className="grid grid-cols-8 gap-1.5 text-gray-700">
|
<Row className="grid grid-cols-8 gap-1.5 text-gray-700">
|
||||||
|
|
|
@ -37,6 +37,7 @@ import {
|
||||||
TruncatedComment,
|
TruncatedComment,
|
||||||
} from 'web/components/feed/feed-comments'
|
} from 'web/components/feed/feed-comments'
|
||||||
import { FeedBet, FeedBetGroup } from 'web/components/feed/feed-bets'
|
import { FeedBet, FeedBetGroup } from 'web/components/feed/feed-bets'
|
||||||
|
import { NumericContract } from 'common/contract'
|
||||||
|
|
||||||
export function FeedItems(props: {
|
export function FeedItems(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -215,8 +216,11 @@ function OutcomeIcon(props: { outcome?: string }) {
|
||||||
function FeedResolve(props: { contract: Contract }) {
|
function FeedResolve(props: { contract: Contract }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
const { creatorName, creatorUsername } = contract
|
const { creatorName, creatorUsername } = contract
|
||||||
|
|
||||||
const resolution = contract.resolution || 'CANCEL'
|
const resolution = contract.resolution || 'CANCEL'
|
||||||
|
|
||||||
|
const resolutionValue = (contract as NumericContract).resolutionValue
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
|
@ -236,6 +240,7 @@ function FeedResolve(props: { contract: Contract }) {
|
||||||
resolved this market to{' '}
|
resolved this market to{' '}
|
||||||
<OutcomeLabel
|
<OutcomeLabel
|
||||||
outcome={resolution}
|
outcome={resolution}
|
||||||
|
value={resolutionValue}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
truncate="long"
|
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 clsx from 'clsx'
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { getProbability } from 'common/calculate'
|
import { getProbability } from 'common/calculate'
|
||||||
|
import { getValueFromBucket } from 'common/calculate-dpm'
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
Contract,
|
Contract,
|
||||||
|
@ -9,6 +10,7 @@ import {
|
||||||
FreeResponse,
|
FreeResponse,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
FullContract,
|
FullContract,
|
||||||
|
NumericContract,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
import { formatPercent } from 'common/util/format'
|
import { formatPercent } from 'common/util/format'
|
||||||
import { ClientRender } from './client-render'
|
import { ClientRender } from './client-render'
|
||||||
|
@ -17,12 +19,20 @@ export function OutcomeLabel(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string
|
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string
|
||||||
truncate: 'short' | 'long' | 'none'
|
truncate: 'short' | 'long' | 'none'
|
||||||
|
value?: number
|
||||||
}) {
|
}) {
|
||||||
const { outcome, contract, truncate } = props
|
const { outcome, contract, truncate, value } = props
|
||||||
|
|
||||||
if (contract.outcomeType === 'BINARY')
|
if (contract.outcomeType === 'BINARY')
|
||||||
return <BinaryOutcomeLabel outcome={outcome as any} />
|
return <BinaryOutcomeLabel outcome={outcome as any} />
|
||||||
|
|
||||||
|
if (contract.outcomeType === 'NUMERIC')
|
||||||
|
return (
|
||||||
|
<span className="text-blue-500">
|
||||||
|
{value ?? getValueFromBucket(outcome, contract as NumericContract)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FreeResponseOutcomeLabel
|
<FreeResponseOutcomeLabel
|
||||||
contract={contract as FullContract<DPM, FreeResponse>}
|
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: {
|
function Button(props: {
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
|
|
|
@ -43,6 +43,7 @@ export const createAnswer = cloudFunction<
|
||||||
export const resolveMarket = cloudFunction<
|
export const resolveMarket = cloudFunction<
|
||||||
{
|
{
|
||||||
outcome: string
|
outcome: string
|
||||||
|
value?: number
|
||||||
contractId: string
|
contractId: string
|
||||||
probabilityInt?: number
|
probabilityInt?: number
|
||||||
resolutions?: { [outcome: string]: number }
|
resolutions?: { [outcome: string]: number }
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import { ArrowLeftIcon } from '@heroicons/react/outline'
|
import { ArrowLeftIcon } from '@heroicons/react/outline'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
import { useContractWithPreload } from 'web/hooks/use-contract'
|
import { useContractWithPreload } from 'web/hooks/use-contract'
|
||||||
import { ContractOverview } from 'web/components/contract/contract-overview'
|
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 { AnswersPanel } from 'web/components/answers/answers-panel'
|
||||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||||
import { Leaderboard } from 'web/components/leaderboard'
|
import { Leaderboard } from 'web/components/leaderboard'
|
||||||
import _ from 'lodash'
|
|
||||||
import { resolvedPayout } from 'common/calculate'
|
import { resolvedPayout } from 'common/calculate'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { useUserById } from 'web/hooks/use-users'
|
import { useUserById } from 'web/hooks/use-users'
|
||||||
import { ContractTabs } from 'web/components/contract/contract-tabs'
|
import { ContractTabs } from 'web/components/contract/contract-tabs'
|
||||||
import { FirstArgument } from 'common/util/types'
|
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 { contractTextDetails } from 'web/components/contract/contract-details'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
import Confetti from 'react-confetti'
|
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 { FeedComment } from 'web/components/feed/feed-comments'
|
||||||
import { FeedBet } from 'web/components/feed/feed-bets'
|
import { FeedBet } from 'web/components/feed/feed-bets'
|
||||||
|
|
||||||
|
@ -113,22 +121,40 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
|
||||||
return <Custom404 />
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
|
|
||||||
const { creatorId, isResolved, question, outcomeType, resolution } = contract
|
const { creatorId, isResolved, question, outcomeType } = contract
|
||||||
|
|
||||||
const isCreator = user?.id === creatorId
|
const isCreator = user?.id === creatorId
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
const isNumeric = outcomeType === 'NUMERIC'
|
||||||
const allowTrade = tradingAllowed(contract)
|
const allowTrade = tradingAllowed(contract)
|
||||||
const allowResolve = !isResolved && isCreator && !!user
|
const allowResolve = !isResolved && isCreator && !!user
|
||||||
const hasSidePanel = isBinary && (allowTrade || allowResolve)
|
const hasSidePanel = (isBinary || isNumeric) && (allowTrade || allowResolve)
|
||||||
|
|
||||||
const ogCardProps = getOpenGraphProps(contract)
|
const ogCardProps = getOpenGraphProps(contract)
|
||||||
|
|
||||||
const rightSidebar = hasSidePanel ? (
|
const rightSidebar = hasSidePanel ? (
|
||||||
<Col className="gap-4">
|
<Col className="gap-4">
|
||||||
{allowTrade && (
|
{allowTrade &&
|
||||||
<BetPanel className="hidden xl:flex" contract={contract} />
|
(isNumeric ? (
|
||||||
)}
|
<NumericBetPanel
|
||||||
{allowResolve && <ResolutionPanel creator={user} contract={contract} />}
|
className="hidden xl:flex"
|
||||||
|
contract={contract as NumericContract}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<BetPanel className="hidden xl:flex" contract={contract} />
|
||||||
|
))}
|
||||||
|
{allowResolve &&
|
||||||
|
(isNumeric ? (
|
||||||
|
<NumericResolutionPanel
|
||||||
|
creator={user}
|
||||||
|
contract={contract as NumericContract}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ResolutionPanel
|
||||||
|
creator={user}
|
||||||
|
contract={contract as BinaryContract}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
) : null
|
) : null
|
||||||
|
|
||||||
|
@ -179,6 +205,13 @@ export function ContractPageContent(props: FirstArgument<typeof ContractPage>) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isNumeric && (
|
||||||
|
<NumericBetPanel
|
||||||
|
className="sm:hidden"
|
||||||
|
contract={contract as NumericContract}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isResolved && (
|
{isResolved && (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2">
|
<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 [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
||||||
const [initialProb, setInitialProb] = useState(50)
|
const [initialProb, setInitialProb] = useState(50)
|
||||||
|
const [minString, setMinString] = useState('')
|
||||||
|
const [maxString, setMaxString] = useState('')
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
|
|
||||||
const [category, setCategory] = useState<string>('')
|
const [category, setCategory] = useState<string>('')
|
||||||
|
@ -94,6 +96,9 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
|
|
||||||
const balance = creator?.balance || 0
|
const balance = creator?.balance || 0
|
||||||
|
|
||||||
|
const min = minString ? parseFloat(minString) : undefined
|
||||||
|
const max = maxString ? parseFloat(maxString) : undefined
|
||||||
|
|
||||||
const isValid =
|
const isValid =
|
||||||
initialProb > 0 &&
|
initialProb > 0 &&
|
||||||
initialProb < 100 &&
|
initialProb < 100 &&
|
||||||
|
@ -104,7 +109,14 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
(ante <= balance || deservesDailyFreeMarket) &&
|
(ante <= balance || deservesDailyFreeMarket) &&
|
||||||
// closeTime must be in the future
|
// closeTime must be in the future
|
||||||
closeTime &&
|
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() {
|
async function submit() {
|
||||||
// TODO: Tell users why their contract is invalid
|
// TODO: Tell users why their contract is invalid
|
||||||
|
@ -121,6 +133,8 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
ante,
|
ante,
|
||||||
closeTime,
|
closeTime,
|
||||||
tags: category ? [category] : undefined,
|
tags: category ? [category] : undefined,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
})
|
})
|
||||||
).then((r) => r.data || {})
|
).then((r) => r.data || {})
|
||||||
|
|
||||||
|
@ -168,6 +182,17 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
/>
|
/>
|
||||||
<span className="label-text">Free response</span>
|
<span className="label-text">Free response</span>
|
||||||
</label>
|
</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>
|
</Row>
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
@ -184,6 +209,40 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
</div>
|
</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} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
<div className="form-control mb-1 items-start">
|
<div className="form-control mb-1 items-start">
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
DPM,
|
DPM,
|
||||||
FreeResponse,
|
FreeResponse,
|
||||||
FullContract,
|
FullContract,
|
||||||
|
NumericContract,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
import { DOMAIN } from 'common/envs/constants'
|
import { DOMAIN } from 'common/envs/constants'
|
||||||
import { AnswersGraph } from 'web/components/answers/answers-graph'
|
import { AnswersGraph } from 'web/components/answers/answers-graph'
|
||||||
|
@ -12,6 +13,7 @@ import BetRow from 'web/components/bet-row'
|
||||||
import {
|
import {
|
||||||
BinaryResolutionOrChance,
|
BinaryResolutionOrChance,
|
||||||
FreeResponseResolutionOrChance,
|
FreeResponseResolutionOrChance,
|
||||||
|
NumericResolutionOrExpectation,
|
||||||
} from 'web/components/contract/contract-card'
|
} from 'web/components/contract/contract-card'
|
||||||
import { ContractDetails } from 'web/components/contract/contract-details'
|
import { ContractDetails } from 'web/components/contract/contract-details'
|
||||||
import { ContractProbGraph } from 'web/components/contract/contract-prob-graph'
|
import { ContractProbGraph } from 'web/components/contract/contract-prob-graph'
|
||||||
|
@ -129,6 +131,12 @@ function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
||||||
truncate="long"
|
truncate="long"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{outcomeType === 'NUMERIC' && resolution && (
|
||||||
|
<NumericResolutionOrExpectation
|
||||||
|
contract={contract as NumericContract}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Spacer h={2} />
|
<Spacer h={2} />
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import Router from 'next/router'
|
import Router from 'next/router'
|
||||||
|
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
|
Loading…
Reference in New Issue
Block a user