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 = {
|
||||
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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user