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 = {
id: string
number: number
contractId: string
createdTime: number
@ -17,7 +18,8 @@ export const getNoneAnswer = (contractId: string, creator: User) => {
const { username, name, avatarUrl } = creator
return {
id: 'NONE',
id: '0',
number: 0,
contractId,
createdTime: Date.now(),
userId: creator.id,

View File

@ -79,9 +79,9 @@ export function getFreeAnswerAnte(
contractId: contract.id,
amount,
shares,
outcome: 'NONE',
probBefore: 100,
probAfter: 100,
outcome: '0',
probBefore: 1,
probAfter: 1,
createdTime,
isAnte: true,
}

View File

@ -80,7 +80,7 @@ export function calculateMoneyRatio<T extends 'BINARY' | 'MULTI'>(
const p = getOutcomeProbability(totalShares, outcome)
const actual = pool.YES + pool.NO - shareValue
const actual = _.sum(Object.values(pool)) - shareValue
const betAmount = p * amount
@ -142,15 +142,15 @@ export function calculateStandardPayout(
const { amount, outcome: betOutcome, shares } = bet
if (betOutcome !== outcome) return 0
const { totalShares, phantomShares } = contract
const { totalShares, phantomShares, pool } = contract
if (!totalShares[outcome]) return 0
const pool = _.sum(Object.values(totalShares))
const poolTotal = _.sum(Object.values(pool))
const total =
totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0)
const winnings = (shares / total) * pool
const winnings = (shares / total) * poolTotal
// profit can be negative if using phantom shares
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) => {
return {
pool: { NONE: ante },
totalShares: { NONE: ante },
totalBets: { NONE: ante },
pool: { '0': ante },
totalShares: { '0': ante },
totalBets: { '0': ante },
phantomShares: undefined,
outcomes: 'FREE_ANSWER' as const,
}

View File

@ -5,6 +5,7 @@ import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { getNewMultiBetInfo } from '../../common/new-bet'
import { Answer } from '../../common/answer'
import { getValues } from './utils'
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
async (
@ -56,15 +57,29 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
if (closeTime && Date.now() > closeTime)
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
.collection(`contracts/${contractId}/answers`)
.doc()
.doc(id)
const answerId = newAnswerDoc.id
const { username, name, avatarUrl } = user
const answer: Answer = {
id: answerId,
id,
number,
contractId,
createdTime: Date.now(),
userId: user.id,

View File

@ -107,9 +107,9 @@ export const createContract = functions
await yesBetDoc.set(yesBet)
await noBetDoc.set(noBet)
} else if (outcomeType === 'MULTI') {
const noneAnswerDoc = firestore.doc(
`contracts/${contract.id}/answers/NONE`
)
const noneAnswerDoc = firestore
.collection(`contracts/${contract.id}/answers`)
.doc('0')
const noneAnswer = getNoneAnswer(contract.id, creator)
await noneAnswerDoc.set(noneAnswer)

View File

@ -1,4 +1,5 @@
import clsx from 'clsx'
import _ from 'lodash'
import { useEffect, useRef, useState } from 'react'
import Textarea from 'react-expanding-textarea'
import { XIcon } from '@heroicons/react/solid'
@ -30,16 +31,23 @@ import {
} from '../../common/calculate'
import { firebaseLogin } from '../lib/firebase/users'
import { Bet } from '../../common/bet'
import { useAnswers } from '../hooks/use-answers'
export function AnswersPanel(props: {
contract: Contract<'MULTI'>
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 (
<Col className="gap-4">
{answers.map((answer) => (
{sortedAnswers.map((answer) => (
<AnswerItem key={answer.id} answer={answer} contract={contract} />
))}
<CreateAnswerInput contract={contract} />
@ -49,7 +57,7 @@ export function AnswersPanel(props: {
function AnswerItem(props: { answer: Answer; contract: Contract<'MULTI'> }) {
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 prob = getOutcomeProbability(contract.totalShares, answer.id)
@ -58,48 +66,46 @@ function AnswerItem(props: { answer: Answer; contract: Contract<'MULTI'> }) {
const [isBetting, setIsBetting] = useState(false)
return (
<Col className="p-2 sm:flex-row">
<Row className="flex-1">
<Col className="gap-2 flex-1">
<div>{answer.text}</div>
<Col className="px-4 py-2 sm:flex-row bg-gray-50 rounded">
<Col className="gap-3 flex-1">
<div>{text}</div>
<Row className="text-gray-500 text-sm gap-2 items-center">
<SiteLink className="relative" href={`/${username}`}>
<Row className="items-center gap-2">
<Avatar avatarUrl={avatarUrl} size={6} />
<div className="truncate">{name}</div>
</Row>
</SiteLink>
<Row className="text-gray-500 text-sm gap-2 items-center">
<SiteLink className="relative" href={`/${username}`}>
<Row className="items-center gap-2">
<Avatar avatarUrl={avatarUrl} size={6} />
<div className="truncate">{name}</div>
</Row>
</SiteLink>
<div className=""></div>
<div className=""></div>
<div className="whitespace-nowrap">
<DateTimeTooltip text="" time={contract.createdTime}>
{createdDate}
</DateTimeTooltip>
</div>
</Row>
</Col>
<div className="whitespace-nowrap">
<DateTimeTooltip text="" time={contract.createdTime}>
{createdDate}
</DateTimeTooltip>
</div>
<div className=""></div>
<div className="text-base">#{number}</div>
</Row>
</Col>
{!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 && (
{isBetting ? (
<AnswerBetPanel
answer={answer}
contract={contract}
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>
)
@ -178,7 +184,7 @@ function AnswerBetPanel(props: {
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
return (
<Col className="items-start">
<Col className="items-start px-2 pb-2">
<Row className="self-stretch items-center justify-between">
<div className="text-xl">Buy this answer</div>
@ -209,7 +215,7 @@ function AnswerBetPanel(props: {
<Spacer h={4} />
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
Potential payout
Payout if chosen
<InfoTooltip
text={`Current payout for ${formatWithCommas(
shares
@ -302,7 +308,7 @@ function CreateAnswerInput(props: { contract: Contract<'MULTI'> }) {
Submit answer & bet
</button>
</Col>
<Col className={clsx('gap-2', text ? 'visible' : 'invisible')}>
<Col className="gap-2">
<div className="text-gray-500 text-sm">Bet amount</div>
<AmountInput
amount={betAmount}

View File

@ -88,7 +88,7 @@ export const ContractOverview = (props: {
{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 ? (
<TagsInput className={clsx('mx-4')} contract={contract} />
) : (
@ -97,7 +97,7 @@ export const ContractOverview = (props: {
<TweetButton tweetText={tweetText} />
</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} />
{folds.length === 0 ? (
<TagsInput contract={contract} />
@ -107,7 +107,7 @@ export const ContractOverview = (props: {
</Col>
{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 */}

View File

@ -6,7 +6,8 @@ export function OutcomeLabel(props: {
if (outcome === 'YES') return <YesLabel />
if (outcome === 'NO') return <NoLabel />
if (outcome === 'MKT') return <ProbLabel />
return <CancelLabel />
if (outcome === 'CANCEL') return <CancelLabel />
return <AnswerNumberLabel number={outcome} />
}
export function YesLabel() {
@ -24,3 +25,7 @@ export function CancelLabel() {
export function ProbLabel() {
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 { db } from './init'
import { User } from '../../../common/user'
import { Answer } from '../../../common/answer'
function getAnswersCollection(contractId: string) {
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) {
const answers = await getValues<Answer>(getAnswersCollection(contractId))
answers.sort((c1, c2) => c1.createdTime - c2.createdTime)

View File

@ -127,6 +127,7 @@ export default function ContractPage(props: {
contract={contract as any}
answers={props.answers}
/>
<div className="divider before:bg-gray-300 after:bg-gray-300" />
</>
)}
</ContractOverview>