Numbered answers. Layout & calculation tweaks

This commit is contained in:
James Grugett 2022-02-14 22:27:15 -06:00
parent 75aa946bd0
commit f06709c20f
11 changed files with 89 additions and 84 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,10 +66,9 @@ 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}`}>
@ -78,28 +85,27 @@ function AnswerItem(props: { answer: Answer; contract: Contract<'MULTI'> }) {
{createdDate} {createdDate}
</DateTimeTooltip> </DateTimeTooltip>
</div> </div>
<div className=""></div>
<div className="text-base">#{number}</div>
</Row> </Row>
</Col> </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}

View File

@ -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 */}

View File

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

View File

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

View File

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