Merge branch 'main' into loans2

This commit is contained in:
James Grugett 2022-08-15 22:07:52 -05:00
commit ef7763eb63
11 changed files with 181 additions and 138 deletions

View File

@ -14,19 +14,21 @@ export type Bet = {
probBefore: number
probAfter: number
sale?: {
amount: number // amount user makes from sale
betId: string // id of bet being sold
// TODO: add sale time?
}
fees: Fees
isSold?: boolean // true if this BUY bet has been sold
isAnte?: boolean
isLiquidityProvision?: boolean
isRedemption?: boolean
challengeSlug?: string
// Props for bets in DPM contract below.
// A bet is either a BUY or a SELL that sells all of a previous buy.
isSold?: boolean // true if this BUY bet has been sold
// This field marks a SELL bet.
sale?: {
amount: number // amount user makes from sale
betId: string // id of BUY bet being sold
}
} & Partial<LimitProps>
export type NumericBet = Bet & {

View File

@ -1,4 +1,4 @@
import { maxBy } from 'lodash'
import { maxBy, sortBy, sum, sumBy } from 'lodash'
import { Bet, LimitBet } from './bet'
import {
calculateCpmmSale,
@ -133,10 +133,46 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
: calculateDpmPayout(contract, bet, outcome)
}
function getCpmmInvested(yourBets: Bet[]) {
const totalShares: { [outcome: string]: number } = {}
const totalSpent: { [outcome: string]: number } = {}
const sortedBets = sortBy(yourBets, 'createdTime')
for (const bet of sortedBets) {
const { outcome, shares, amount } = bet
if (amount > 0) {
totalShares[outcome] = (totalShares[outcome] ?? 0) + shares
totalSpent[outcome] = (totalSpent[outcome] ?? 0) + amount
} else if (amount < 0) {
const averagePrice = totalSpent[outcome] / totalShares[outcome]
totalShares[outcome] = totalShares[outcome] + shares
totalSpent[outcome] = totalSpent[outcome] + averagePrice * shares
}
}
return sum(Object.values(totalSpent))
}
function getDpmInvested(yourBets: Bet[]) {
const sortedBets = sortBy(yourBets, 'createdTime')
return sumBy(sortedBets, (bet) => {
const { amount, sale } = bet
if (sale) {
const originalBet = sortedBets.find((b) => b.id === sale.betId)
if (originalBet) return -originalBet.amount
return 0
}
return amount
})
}
export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
const { resolution } = contract
const isCpmm = contract.mechanism === 'cpmm-1'
let currentInvested = 0
let totalInvested = 0
let payout = 0
let loan = 0
@ -162,7 +198,6 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
saleValue -= amount
}
currentInvested += amount
loan += loanAmount ?? 0
payout += resolution
? calculatePayout(contract, bet, resolution)
@ -174,12 +209,13 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
const profit = payout + saleValue + redeemed - totalInvested
const profitPercent = (profit / totalInvested) * 100
const invested = isCpmm ? getCpmmInvested(yourBets) : getDpmInvested(yourBets)
const hasShares = Object.values(totalShares).some(
(shares) => !floatingEqual(shares, 0)
)
return {
invested: Math.max(0, currentInvested),
invested,
payout,
netPayout,
profit,

View File

@ -1,6 +1,6 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
import { difference, mapValues, groupBy, sumBy } from 'lodash'
import {
Contract,
@ -22,6 +22,8 @@ import { isManifoldId } from '../../common/envs/constants'
import { removeUndefinedProps } from '../../common/util/object'
import { LiquidityProvision } from '../../common/liquidity-provision'
import { APIError, newEndpoint, validate } from './api'
import { getContractBetMetrics } from '../../common/calculate'
import { floatingEqual } from '../../common/util/math'
const bodySchema = z.object({
contractId: z.string(),
@ -162,7 +164,7 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => {
const userPayoutsWithoutLoans = groupPayoutsByUser(payouts)
await sendResolutionEmails(
openBets,
bets,
userPayoutsWithoutLoans,
creator,
creatorPayout,
@ -188,7 +190,7 @@ const processPayouts = async (payouts: Payout[], isDeposit = false) => {
}
const sendResolutionEmails = async (
openBets: Bet[],
bets: Bet[],
userPayouts: { [userId: string]: number },
creator: User,
creatorPayout: number,
@ -197,14 +199,15 @@ const sendResolutionEmails = async (
resolutionProbability?: number,
resolutions?: { [outcome: string]: number }
) => {
const nonWinners = difference(
uniq(openBets.map(({ userId }) => userId)),
Object.keys(userPayouts)
)
const investedByUser = mapValues(
groupBy(openBets, (bet) => bet.userId),
(bets) => sumBy(bets, (bet) => bet.amount)
groupBy(bets, (bet) => bet.userId),
(bets) => getContractBetMetrics(contract, bets).invested
)
const investedUsers = Object.keys(investedByUser).filter(
(userId) => !floatingEqual(investedByUser[userId], 0)
)
const nonWinners = difference(investedUsers, Object.keys(userPayouts))
const emailPayouts = [
...Object.entries(userPayouts),
...nonWinners.map((userId) => [userId, 0] as const),

View File

@ -1,6 +1,6 @@
import Router from 'next/router'
import clsx from 'clsx'
import { MouseEvent } from 'react'
import { MouseEvent, useState } from 'react'
import { UserCircleIcon, UserIcon, UsersIcon } from '@heroicons/react/solid'
export function Avatar(props: {
@ -10,7 +10,8 @@ export function Avatar(props: {
size?: number | 'xs' | 'sm'
className?: string
}) {
const { username, avatarUrl, noLink, size, className } = props
const { username, noLink, size, className } = props
const [avatarUrl, setAvatarUrl] = useState(props.avatarUrl)
const s = size == 'xs' ? 6 : size === 'sm' ? 8 : size || 10
const onClick =
@ -35,6 +36,11 @@ export function Avatar(props: {
src={avatarUrl}
onClick={onClick}
alt={username}
onError={() => {
// If the image doesn't load, clear the avatarUrl to show the default
// Mostly for localhost, when getting a 403 from googleusercontent
setAvatarUrl('')
}}
/>
) : (
<UserCircleIcon

View File

@ -428,95 +428,105 @@ export function BetsSummary(props: {
: 'NO'
: 'YES'
const canSell =
isYourBets &&
isCpmm &&
(isBinary || isPseudoNumeric) &&
!isClosed &&
!resolution &&
hasShares &&
sharesOutcome &&
user
return (
<Row className={clsx('flex-wrap gap-4 sm:flex-nowrap sm:gap-6', className)}>
{!isCpmm && (
<Col className={clsx(className, 'gap-4')}>
<Row className="flex-wrap gap-4 sm:flex-nowrap sm:gap-6">
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Invested
</div>
<div className="whitespace-nowrap">{formatMoney(invested)}</div>
</Col>
)}
{resolution ? (
<Col>
<div className="text-sm text-gray-500">Payout</div>
<div className="whitespace-nowrap text-sm text-gray-500">Profit</div>
<div className="whitespace-nowrap">
{formatMoney(payout)} <ProfitBadge profitPercent={profitPercent} />
{formatMoney(profit)} <ProfitBadge profitPercent={profitPercent} />
</div>
</Col>
) : isBinary ? (
<>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <YesLabel />
</div>
<div className="whitespace-nowrap">{formatMoney(yesWinnings)}</div>
</Col>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <NoLabel />
</div>
<div className="whitespace-nowrap">{formatMoney(noWinnings)}</div>
</Col>
</>
) : isPseudoNumeric ? (
<>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if {'>='} {formatLargeNumber(contract.max)}
</div>
<div className="whitespace-nowrap">{formatMoney(yesWinnings)}</div>
</Col>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if {'<='} {formatLargeNumber(contract.min)}
</div>
<div className="whitespace-nowrap">{formatMoney(noWinnings)}</div>
</Col>
</>
) : (
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Current value
</div>
<div className="whitespace-nowrap">{formatMoney(payout)}</div>
</Col>
)}
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">Profit</div>
<div className="whitespace-nowrap">
{formatMoney(profit)} <ProfitBadge profitPercent={profitPercent} />
{isYourBets &&
isCpmm &&
(isBinary || isPseudoNumeric) &&
!isClosed &&
!resolution &&
hasShares &&
sharesOutcome &&
user && (
<>
<button
className="btn btn-sm ml-2"
onClick={() => setShowSellModal(true)}
>
Sell
</button>
{showSellModal && (
<SellSharesModal
contract={contract}
user={user}
userBets={bets}
shares={totalShares[sharesOutcome]}
sharesOutcome={sharesOutcome}
setOpen={setShowSellModal}
/>
)}
</>
{canSell && (
<>
<button
className="btn btn-sm self-end"
onClick={() => setShowSellModal(true)}
>
Sell
</button>
{showSellModal && (
<SellSharesModal
contract={contract}
user={user}
userBets={bets}
shares={totalShares[sharesOutcome]}
sharesOutcome={sharesOutcome}
setOpen={setShowSellModal}
/>
)}
</div>
</Col>
</Row>
</>
)}
</Row>
<Row className="flex-wrap-none gap-4">
{resolution ? (
<Col>
<div className="text-sm text-gray-500">Payout</div>
<div className="whitespace-nowrap">
{formatMoney(payout)}{' '}
<ProfitBadge profitPercent={profitPercent} />
</div>
</Col>
) : isBinary ? (
<>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <YesLabel />
</div>
<div className="whitespace-nowrap">
{formatMoney(yesWinnings)}
</div>
</Col>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <NoLabel />
</div>
<div className="whitespace-nowrap">{formatMoney(noWinnings)}</div>
</Col>
</>
) : isPseudoNumeric ? (
<>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if {'>='} {formatLargeNumber(contract.max)}
</div>
<div className="whitespace-nowrap">
{formatMoney(yesWinnings)}
</div>
</Col>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if {'<='} {formatLargeNumber(contract.min)}
</div>
<div className="whitespace-nowrap">{formatMoney(noWinnings)}</div>
</Col>
</>
) : (
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Current value
</div>
<div className="whitespace-nowrap">{formatMoney(payout)}</div>
</Col>
)}
</Row>
</Col>
)
}

View File

@ -83,7 +83,7 @@ export function ContractSearch(props: {
highlightOptions?: ContractHighlightOptions
onContractClick?: (contract: Contract) => void
hideOrderSelector?: boolean
overrideGridClassName?: string
gridClassName?: string
cardHideOptions?: {
hideGroupLink?: boolean
hideQuickBet?: boolean
@ -98,7 +98,7 @@ export function ContractSearch(props: {
defaultFilter,
additionalFilter,
onContractClick,
overrideGridClassName,
gridClassName,
hideOrderSelector,
cardHideOptions,
highlightOptions,
@ -181,7 +181,7 @@ export function ContractSearch(props: {
loadMore={performQuery}
showTime={showTime}
onContractClick={onContractClick}
overrideGridClassName={overrideGridClassName}
gridClassName={gridClassName}
highlightOptions={highlightOptions}
cardHideOptions={cardHideOptions}
/>

View File

@ -20,7 +20,7 @@ export function ContractsGrid(props: {
loadMore?: () => void
showTime?: ShowTime
onContractClick?: (contract: Contract) => void
overrideGridClassName?: string
gridClassName?: string
cardHideOptions?: {
hideQuickBet?: boolean
hideGroupLink?: boolean
@ -32,7 +32,7 @@ export function ContractsGrid(props: {
showTime,
loadMore,
onContractClick,
overrideGridClassName,
gridClassName,
cardHideOptions,
highlightOptions,
} = props
@ -66,9 +66,8 @@ export function ContractsGrid(props: {
<Col className="gap-8">
<ul
className={clsx(
overrideGridClassName
? overrideGridClassName
: 'grid w-full grid-cols-1 gap-4 md:grid-cols-2'
'w-full columns-1 gap-4 space-y-4 md:columns-2',
gridClassName
)}
>
{contracts.map((contract) => (
@ -81,11 +80,10 @@ export function ContractsGrid(props: {
}
hideQuickBet={hideQuickBet}
hideGroupLink={hideGroupLink}
className={
contractIds?.includes(contract.id)
? highlightClassName
: undefined
}
className={clsx(
'break-inside-avoid-column',
contractIds?.includes(contract.id) && highlightClassName
)}
/>
))}
</ul>

View File

@ -65,9 +65,7 @@ export function MarketModal(props: {
<ContractSearch
hideOrderSelector
onContractClick={addContract}
overrideGridClassName={
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
}
gridClassName="gap-3 space-y-3"
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
highlightOptions={{
contractIds: contracts.map((c) => c.id),

View File

@ -266,12 +266,16 @@ export function listenForHotContracts(
})
}
export async function getHotContracts() {
const data = await getValues<Contract>(hotContractsQuery)
return sortBy(
chooseRandomSubset(data, 10),
(contract) => -1 * contract.volume24Hours
)
const trendingContractsQuery = query(
contracts,
where('isResolved', '==', false),
where('visibility', '==', 'public'),
orderBy('popularityScore', 'desc'),
limit(10)
)
export async function getTrendingContracts() {
return await getValues<Contract>(trendingContractsQuery)
}
export async function getContractsBySlugs(slugs: string[]) {

View File

@ -607,9 +607,7 @@ function AddContractButton(props: { group: Group; user: User }) {
user={user}
hideOrderSelector={true}
onContractClick={addContractToCurrentGroup}
overrideGridClassName={
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
}
gridClassName="gap-3 space-y-3"
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
additionalFilter={{ excludeContractIds: group.contractIds }}
highlightOptions={{

View File

@ -1,4 +1,4 @@
import { Contract, getContractsBySlugs } from 'web/lib/firebase/contracts'
import { Contract, getTrendingContracts } from 'web/lib/firebase/contracts'
import { Page } from 'web/components/page'
import { LandingPagePanel } from 'web/components/landing-page-panel'
import { Col } from 'web/components/layout/col'
@ -8,19 +8,7 @@ import { useSaveReferral } from 'web/hooks/use-save-referral'
import { SEO } from 'web/components/SEO'
export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => {
// These hardcoded markets will be shown in the frontpage for signed-out users:
const hotContracts = await getContractsBySlugs([
'will-max-go-to-prom-with-a-girl',
'will-ethereum-switch-to-proof-of-st',
'will-russia-control-the-majority-of',
'will-elon-musk-buy-twitter-this-yea',
'will-trump-be-charged-by-the-grand',
'will-spacex-launch-a-starship-into',
'who-will-win-the-nba-finals-champio',
'who-will-be-time-magazine-person-of',
'will-congress-hold-any-hearings-abo-e21f987033b3',
'will-at-least-10-world-cities-have',
])
const hotContracts = await getTrendingContracts()
return { props: { hotContracts } }
})