Numbered answers. Layout & calculation tweaks
This commit is contained in:
parent
75aa946bd0
commit
f06709c20f
|
@ -2,6 +2,7 @@ import { User } from './user'
|
||||||
|
|
||||||
export type Answer = {
|
export type Answer = {
|
||||||
id: string
|
id: string
|
||||||
|
number: number
|
||||||
contractId: string
|
contractId: string
|
||||||
createdTime: number
|
createdTime: number
|
||||||
|
|
||||||
|
@ -17,7 +18,8 @@ export const getNoneAnswer = (contractId: string, creator: User) => {
|
||||||
const { username, name, avatarUrl } = creator
|
const { username, name, avatarUrl } = creator
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'NONE',
|
id: '0',
|
||||||
|
number: 0,
|
||||||
contractId,
|
contractId,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
userId: creator.id,
|
userId: creator.id,
|
||||||
|
|
|
@ -79,9 +79,9 @@ export function getFreeAnswerAnte(
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
shares,
|
shares,
|
||||||
outcome: 'NONE',
|
outcome: '0',
|
||||||
probBefore: 100,
|
probBefore: 1,
|
||||||
probAfter: 100,
|
probAfter: 1,
|
||||||
createdTime,
|
createdTime,
|
||||||
isAnte: true,
|
isAnte: true,
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ export function calculateMoneyRatio<T extends 'BINARY' | 'MULTI'>(
|
||||||
|
|
||||||
const p = getOutcomeProbability(totalShares, outcome)
|
const p = getOutcomeProbability(totalShares, outcome)
|
||||||
|
|
||||||
const actual = pool.YES + pool.NO - shareValue
|
const actual = _.sum(Object.values(pool)) - shareValue
|
||||||
|
|
||||||
const betAmount = p * amount
|
const betAmount = p * amount
|
||||||
|
|
||||||
|
@ -142,15 +142,15 @@ export function calculateStandardPayout(
|
||||||
const { amount, outcome: betOutcome, shares } = bet
|
const { amount, outcome: betOutcome, shares } = bet
|
||||||
if (betOutcome !== outcome) return 0
|
if (betOutcome !== outcome) return 0
|
||||||
|
|
||||||
const { totalShares, phantomShares } = contract
|
const { totalShares, phantomShares, pool } = contract
|
||||||
if (!totalShares[outcome]) return 0
|
if (!totalShares[outcome]) return 0
|
||||||
|
|
||||||
const pool = _.sum(Object.values(totalShares))
|
const poolTotal = _.sum(Object.values(pool))
|
||||||
|
|
||||||
const total =
|
const total =
|
||||||
totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0)
|
totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0)
|
||||||
|
|
||||||
const winnings = (shares / total) * pool
|
const winnings = (shares / total) * poolTotal
|
||||||
// profit can be negative if using phantom shares
|
// profit can be negative if using phantom shares
|
||||||
return amount + (1 - FEES) * Math.max(0, winnings - amount)
|
return amount + (1 - FEES) * Math.max(0, winnings - amount)
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,9 +70,9 @@ const getBinaryProps = (initialProb: number, ante: number) => {
|
||||||
|
|
||||||
const getFreeAnswerProps = (ante: number) => {
|
const getFreeAnswerProps = (ante: number) => {
|
||||||
return {
|
return {
|
||||||
pool: { NONE: ante },
|
pool: { '0': ante },
|
||||||
totalShares: { NONE: ante },
|
totalShares: { '0': ante },
|
||||||
totalBets: { NONE: ante },
|
totalBets: { '0': ante },
|
||||||
phantomShares: undefined,
|
phantomShares: undefined,
|
||||||
outcomes: 'FREE_ANSWER' as const,
|
outcomes: 'FREE_ANSWER' as const,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { getNewMultiBetInfo } from '../../common/new-bet'
|
import { getNewMultiBetInfo } from '../../common/new-bet'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
|
import { getValues } from './utils'
|
||||||
|
|
||||||
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
async (
|
||||||
|
@ -56,15 +57,29 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (closeTime && Date.now() > closeTime)
|
||||||
return { status: 'error', message: 'Trading is closed' }
|
return { status: 'error', message: 'Trading is closed' }
|
||||||
|
|
||||||
|
const [lastAnswer] = await getValues<Answer>(
|
||||||
|
firestore
|
||||||
|
.collection(`contracts/${contractId}/answers`)
|
||||||
|
.orderBy('number', 'desc')
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!lastAnswer)
|
||||||
|
return { status: 'error', message: 'Could not fetch last answer' }
|
||||||
|
|
||||||
|
const number = lastAnswer.number + 1
|
||||||
|
const id = `${number}`
|
||||||
|
|
||||||
const newAnswerDoc = firestore
|
const newAnswerDoc = firestore
|
||||||
.collection(`contracts/${contractId}/answers`)
|
.collection(`contracts/${contractId}/answers`)
|
||||||
.doc()
|
.doc(id)
|
||||||
|
|
||||||
const answerId = newAnswerDoc.id
|
const answerId = newAnswerDoc.id
|
||||||
const { username, name, avatarUrl } = user
|
const { username, name, avatarUrl } = user
|
||||||
|
|
||||||
const answer: Answer = {
|
const answer: Answer = {
|
||||||
id: answerId,
|
id,
|
||||||
|
number,
|
||||||
contractId,
|
contractId,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
|
@ -107,9 +107,9 @@ export const createContract = functions
|
||||||
await yesBetDoc.set(yesBet)
|
await yesBetDoc.set(yesBet)
|
||||||
await noBetDoc.set(noBet)
|
await noBetDoc.set(noBet)
|
||||||
} else if (outcomeType === 'MULTI') {
|
} else if (outcomeType === 'MULTI') {
|
||||||
const noneAnswerDoc = firestore.doc(
|
const noneAnswerDoc = firestore
|
||||||
`contracts/${contract.id}/answers/NONE`
|
.collection(`contracts/${contract.id}/answers`)
|
||||||
)
|
.doc('0')
|
||||||
const noneAnswer = getNoneAnswer(contract.id, creator)
|
const noneAnswer = getNoneAnswer(contract.id, creator)
|
||||||
await noneAnswerDoc.set(noneAnswer)
|
await noneAnswerDoc.set(noneAnswer)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import _ from 'lodash'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import Textarea from 'react-expanding-textarea'
|
import Textarea from 'react-expanding-textarea'
|
||||||
import { XIcon } from '@heroicons/react/solid'
|
import { XIcon } from '@heroicons/react/solid'
|
||||||
|
@ -30,16 +31,23 @@ import {
|
||||||
} from '../../common/calculate'
|
} from '../../common/calculate'
|
||||||
import { firebaseLogin } from '../lib/firebase/users'
|
import { firebaseLogin } from '../lib/firebase/users'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
|
import { useAnswers } from '../hooks/use-answers'
|
||||||
|
|
||||||
export function AnswersPanel(props: {
|
export function AnswersPanel(props: {
|
||||||
contract: Contract<'MULTI'>
|
contract: Contract<'MULTI'>
|
||||||
answers: Answer[]
|
answers: Answer[]
|
||||||
}) {
|
}) {
|
||||||
const { contract, answers } = props
|
const { contract } = props
|
||||||
|
|
||||||
|
const answers = useAnswers(contract.id) ?? props.answers
|
||||||
|
const sortedAnswers = _.sortBy(
|
||||||
|
answers,
|
||||||
|
(answer) => -1 * getOutcomeProbability(contract.totalShares, answer.id)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-4">
|
<Col className="gap-4">
|
||||||
{answers.map((answer) => (
|
{sortedAnswers.map((answer) => (
|
||||||
<AnswerItem key={answer.id} answer={answer} contract={contract} />
|
<AnswerItem key={answer.id} answer={answer} contract={contract} />
|
||||||
))}
|
))}
|
||||||
<CreateAnswerInput contract={contract} />
|
<CreateAnswerInput contract={contract} />
|
||||||
|
@ -49,7 +57,7 @@ export function AnswersPanel(props: {
|
||||||
|
|
||||||
function AnswerItem(props: { answer: Answer; contract: Contract<'MULTI'> }) {
|
function AnswerItem(props: { answer: Answer; contract: Contract<'MULTI'> }) {
|
||||||
const { answer, contract } = props
|
const { answer, contract } = props
|
||||||
const { username, avatarUrl, name, createdTime } = answer
|
const { username, avatarUrl, name, createdTime, number, text } = answer
|
||||||
|
|
||||||
const createdDate = dayjs(createdTime).format('MMM D')
|
const createdDate = dayjs(createdTime).format('MMM D')
|
||||||
const prob = getOutcomeProbability(contract.totalShares, answer.id)
|
const prob = getOutcomeProbability(contract.totalShares, answer.id)
|
||||||
|
@ -58,48 +66,46 @@ function AnswerItem(props: { answer: Answer; contract: Contract<'MULTI'> }) {
|
||||||
const [isBetting, setIsBetting] = useState(false)
|
const [isBetting, setIsBetting] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="p-2 sm:flex-row">
|
<Col className="px-4 py-2 sm:flex-row bg-gray-50 rounded">
|
||||||
<Row className="flex-1">
|
<Col className="gap-3 flex-1">
|
||||||
<Col className="gap-2 flex-1">
|
<div>{text}</div>
|
||||||
<div>{answer.text}</div>
|
|
||||||
|
|
||||||
<Row className="text-gray-500 text-sm gap-2 items-center">
|
<Row className="text-gray-500 text-sm gap-2 items-center">
|
||||||
<SiteLink className="relative" href={`/${username}`}>
|
<SiteLink className="relative" href={`/${username}`}>
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
<Avatar avatarUrl={avatarUrl} size={6} />
|
<Avatar avatarUrl={avatarUrl} size={6} />
|
||||||
<div className="truncate">{name}</div>
|
<div className="truncate">{name}</div>
|
||||||
</Row>
|
</Row>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
|
|
||||||
<div className="">•</div>
|
<div className="">•</div>
|
||||||
|
|
||||||
<div className="whitespace-nowrap">
|
<div className="whitespace-nowrap">
|
||||||
<DateTimeTooltip text="" time={contract.createdTime}>
|
<DateTimeTooltip text="" time={contract.createdTime}>
|
||||||
{createdDate}
|
{createdDate}
|
||||||
</DateTimeTooltip>
|
</DateTimeTooltip>
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
<div className="">•</div>
|
||||||
</Col>
|
<div className="text-base">#{number}</div>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
{!isBetting && (
|
{isBetting ? (
|
||||||
<Col className="sm:flex-row items-center gap-4">
|
|
||||||
<div className="text-2xl text-green-500">{probPercent}</div>
|
|
||||||
<BuyButton
|
|
||||||
className="justify-end self-end flex-initial btn-md !px-4 sm:!px-8"
|
|
||||||
onClick={() => {
|
|
||||||
setIsBetting(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{isBetting && (
|
|
||||||
<AnswerBetPanel
|
<AnswerBetPanel
|
||||||
answer={answer}
|
answer={answer}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
closePanel={() => setIsBetting(false)}
|
closePanel={() => setIsBetting(false)}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Row className="self-end sm:self-start items-center gap-4">
|
||||||
|
<div className="text-2xl text-green-500">{probPercent}</div>
|
||||||
|
<BuyButton
|
||||||
|
className="justify-end self-end flex-initial btn-md !px-8"
|
||||||
|
onClick={() => {
|
||||||
|
setIsBetting(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
@ -178,7 +184,7 @@ function AnswerBetPanel(props: {
|
||||||
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="items-start">
|
<Col className="items-start px-2 pb-2">
|
||||||
<Row className="self-stretch items-center justify-between">
|
<Row className="self-stretch items-center justify-between">
|
||||||
<div className="text-xl">Buy this answer</div>
|
<div className="text-xl">Buy this answer</div>
|
||||||
|
|
||||||
|
@ -209,7 +215,7 @@ function AnswerBetPanel(props: {
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
||||||
Potential payout
|
Payout if chosen
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
text={`Current payout for ${formatWithCommas(
|
text={`Current payout for ${formatWithCommas(
|
||||||
shares
|
shares
|
||||||
|
@ -302,7 +308,7 @@ function CreateAnswerInput(props: { contract: Contract<'MULTI'> }) {
|
||||||
Submit answer & bet
|
Submit answer & bet
|
||||||
</button>
|
</button>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className={clsx('gap-2', text ? 'visible' : 'invisible')}>
|
<Col className="gap-2">
|
||||||
<div className="text-gray-500 text-sm">Bet amount</div>
|
<div className="text-gray-500 text-sm">Bet amount</div>
|
||||||
<AmountInput
|
<AmountInput
|
||||||
amount={betAmount}
|
amount={betAmount}
|
||||||
|
|
|
@ -88,7 +88,7 @@ export const ContractOverview = (props: {
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
<Row className="mt-6 ml-4 hidden items-center justify-between gap-4 sm:flex">
|
<Row className="mt-6 hidden items-center justify-between gap-4 sm:flex">
|
||||||
{folds.length === 0 ? (
|
{folds.length === 0 ? (
|
||||||
<TagsInput className={clsx('mx-4')} contract={contract} />
|
<TagsInput className={clsx('mx-4')} contract={contract} />
|
||||||
) : (
|
) : (
|
||||||
|
@ -97,7 +97,7 @@ export const ContractOverview = (props: {
|
||||||
<TweetButton tweetText={tweetText} />
|
<TweetButton tweetText={tweetText} />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Col className="mt-6 ml-4 gap-4 sm:hidden">
|
<Col className="mt-6 gap-4 sm:hidden">
|
||||||
<TweetButton className="self-end" tweetText={tweetText} />
|
<TweetButton className="self-end" tweetText={tweetText} />
|
||||||
{folds.length === 0 ? (
|
{folds.length === 0 ? (
|
||||||
<TagsInput contract={contract} />
|
<TagsInput contract={contract} />
|
||||||
|
@ -107,7 +107,7 @@ export const ContractOverview = (props: {
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{folds.length > 0 && (
|
{folds.length > 0 && (
|
||||||
<RevealableTagsInput className="mx-4 mt-4" contract={contract} />
|
<RevealableTagsInput className="mt-4" contract={contract} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Show a delete button for contracts without any trading */}
|
{/* Show a delete button for contracts without any trading */}
|
||||||
|
|
|
@ -6,7 +6,8 @@ export function OutcomeLabel(props: {
|
||||||
if (outcome === 'YES') return <YesLabel />
|
if (outcome === 'YES') return <YesLabel />
|
||||||
if (outcome === 'NO') return <NoLabel />
|
if (outcome === 'NO') return <NoLabel />
|
||||||
if (outcome === 'MKT') return <ProbLabel />
|
if (outcome === 'MKT') return <ProbLabel />
|
||||||
return <CancelLabel />
|
if (outcome === 'CANCEL') return <CancelLabel />
|
||||||
|
return <AnswerNumberLabel number={outcome} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export function YesLabel() {
|
export function YesLabel() {
|
||||||
|
@ -24,3 +25,7 @@ export function CancelLabel() {
|
||||||
export function ProbLabel() {
|
export function ProbLabel() {
|
||||||
return <span className="text-blue-400">PROB</span>
|
return <span className="text-blue-400">PROB</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AnswerNumberLabel(props: { number: string }) {
|
||||||
|
return <span className="text-blue-400">#{props.number}</span>
|
||||||
|
}
|
||||||
|
|
|
@ -1,37 +1,13 @@
|
||||||
import { doc, collection, setDoc } from 'firebase/firestore'
|
import { collection } from 'firebase/firestore'
|
||||||
|
|
||||||
import { getValues, listenForValues } from './utils'
|
import { getValues, listenForValues } from './utils'
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
import { User } from '../../../common/user'
|
|
||||||
import { Answer } from '../../../common/answer'
|
import { Answer } from '../../../common/answer'
|
||||||
|
|
||||||
function getAnswersCollection(contractId: string) {
|
function getAnswersCollection(contractId: string) {
|
||||||
return collection(db, 'contracts', contractId, 'answers')
|
return collection(db, 'contracts', contractId, 'answers')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAnswer(
|
|
||||||
contractId: string,
|
|
||||||
text: string,
|
|
||||||
user: User
|
|
||||||
) {
|
|
||||||
const { id: userId, username, name, avatarUrl } = user
|
|
||||||
|
|
||||||
const ref = doc(getAnswersCollection(contractId))
|
|
||||||
|
|
||||||
const answer: Answer = {
|
|
||||||
id: ref.id,
|
|
||||||
contractId,
|
|
||||||
createdTime: Date.now(),
|
|
||||||
userId,
|
|
||||||
username,
|
|
||||||
name,
|
|
||||||
avatarUrl,
|
|
||||||
text,
|
|
||||||
}
|
|
||||||
|
|
||||||
return await setDoc(ref, answer)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listAllAnswers(contractId: string) {
|
export async function listAllAnswers(contractId: string) {
|
||||||
const answers = await getValues<Answer>(getAnswersCollection(contractId))
|
const answers = await getValues<Answer>(getAnswersCollection(contractId))
|
||||||
answers.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
answers.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||||
|
|
|
@ -127,6 +127,7 @@ export default function ContractPage(props: {
|
||||||
contract={contract as any}
|
contract={contract as any}
|
||||||
answers={props.answers}
|
answers={props.answers}
|
||||||
/>
|
/>
|
||||||
|
<div className="divider before:bg-gray-300 after:bg-gray-300" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ContractOverview>
|
</ContractOverview>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user