Implement leaderboards for folds!

This commit is contained in:
jahooma 2022-01-22 17:59:50 -06:00
parent bc1decdbfc
commit aa1022546d
6 changed files with 147 additions and 46 deletions

View File

@ -11,7 +11,6 @@ export type Contract = {
description: string // More info about what the contract is about
tags: string[]
outcomeType: 'BINARY' // | 'MULTI' | 'interval' | 'date'
// outcomes: ['YES', 'NO']
visibility: 'public' | 'unlisted'
mechanism: 'dpm-2'
@ -26,8 +25,10 @@ export type Contract = {
isResolved: boolean
resolutionTime?: number // When the contract creator resolved the market
resolution?: 'YES' | 'NO' | 'CANCEL' // Chosen by creator; must be one of outcomes
resolution?: outcome // Chosen by creator; must be one of outcomes
volume24Hours: number
volume7Days: number
}
export type outcome = 'YES' | 'NO' | 'CANCEL' | 'MKT'

View File

@ -1,22 +1,23 @@
import { Bet } from './bet'
import { getProbability } from './calculate'
import { Contract } from './contract'
import { Contract, outcome } from './contract'
import { CREATOR_FEE, FEES } from './fees'
export const getCancelPayouts = (truePool: number, bets: Bet[]) => {
console.log('resolved N/A, pool M$', truePool)
export const getCancelPayouts = (contract: Contract, bets: Bet[]) => {
const { pool } = contract
const poolTotal = pool.YES + pool.NO
console.log('resolved N/A, pool M$', poolTotal)
const betSum = sumBy(bets, (b) => b.amount)
return bets.map((bet) => ({
userId: bet.userId,
payout: (bet.amount / betSum) * truePool,
payout: (bet.amount / betSum) * poolTotal,
}))
}
export const getStandardPayouts = (
outcome: string,
truePool: number,
outcome: 'YES' | 'NO',
contract: Contract,
bets: Bet[]
) => {
@ -25,11 +26,13 @@ export const getStandardPayouts = (
const betSum = sumBy(winningBets, (b) => b.amount)
if (betSum >= truePool) return getCancelPayouts(truePool, winningBets)
const poolTotal = contract.pool.YES + contract.pool.NO
if (betSum >= poolTotal) return getCancelPayouts(contract, winningBets)
const shareDifferenceSum = sumBy(winningBets, (b) => b.shares - b.amount)
const winningsPool = truePool - betSum
const winningsPool = poolTotal - betSum
const winnerPayouts = winningBets.map((bet) => ({
userId: bet.userId,
@ -46,7 +49,7 @@ export const getStandardPayouts = (
'resolved',
outcome,
'pool: M$',
truePool,
poolTotal,
'creator fee: M$',
creatorPayout
)
@ -56,13 +59,10 @@ export const getStandardPayouts = (
]) // add creator fee
}
export const getMktPayouts = (
truePool: number,
contract: Contract,
bets: Bet[]
) => {
export const getMktPayouts = (contract: Contract, bets: Bet[]) => {
const p = getProbability(contract.totalShares)
console.log('Resolved MKT at p=', p, 'pool: $M', truePool)
const poolTotal = contract.pool.YES + contract.pool.NO
console.log('Resolved MKT at p=', p, 'pool: $M', poolTotal)
const [yesBets, noBets] = partition(bets, (bet) => bet.outcome === 'YES')
@ -70,17 +70,17 @@ export const getMktPayouts = (
p * sumBy(yesBets, (b) => b.amount) +
(1 - p) * sumBy(noBets, (b) => b.amount)
if (weightedBetTotal >= truePool) {
if (weightedBetTotal >= poolTotal) {
return bets.map((bet) => ({
userId: bet.userId,
payout:
(((bet.outcome === 'YES' ? p : 1 - p) * bet.amount) /
weightedBetTotal) *
truePool,
poolTotal,
}))
}
const winningsPool = truePool - weightedBetTotal
const winningsPool = poolTotal - weightedBetTotal
const weightedShareTotal =
p * sumBy(yesBets, (b) => b.shares - b.amount) +
@ -113,6 +113,22 @@ export const getMktPayouts = (
]
}
export const getPayouts = (
outcome: outcome,
contract: Contract,
bets: Bet[]
) => {
switch (outcome) {
case 'YES':
case 'NO':
return getStandardPayouts(outcome, contract, bets)
case 'MKT':
return getMktPayouts(contract, bets)
case 'CANCEL':
return getCancelPayouts(contract, bets)
}
}
const partition = <T>(array: T[], f: (t: T) => boolean) => {
const yes = []
const no = []

View File

@ -7,11 +7,7 @@ import { User } from '../../common/user'
import { Bet } from '../../common/bet'
import { getUser, payUser } from './utils'
import { sendMarketResolutionEmail } from './emails'
import {
getCancelPayouts,
getMktPayouts,
getStandardPayouts,
} from '../../common/payouts'
import { getPayouts } from '../../common/payouts'
export const resolveMarket = functions
.runWith({ minInstances: 1 })
@ -61,14 +57,7 @@ export const resolveMarket = functions
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
const openBets = bets.filter((b) => !b.isSold && !b.sale)
const truePool = contract.pool.YES + contract.pool.NO
const payouts =
outcome === 'CANCEL'
? getCancelPayouts(truePool, openBets)
: outcome === 'MKT'
? getMktPayouts(truePool, contract, openBets)
: getStandardPayouts(outcome, truePool, contract, openBets)
const payouts = getPayouts(outcome, contract, openBets)
console.log('payouts:', payouts)

View File

@ -1,4 +1,3 @@
import Image from 'next/image'
import { User } from '../../common/user'
import { Row } from './layout/row'
import { SiteLink } from './site-link'
@ -34,7 +33,7 @@ export function Leaderboard(props: {
<td>
<SiteLink className="relative" href={`/${user.username}`}>
<Row className="items-center gap-4">
<Image
<img
className="rounded-full bg-gray-400 flex-shrink-0 ring-8 ring-gray-50"
src={user.avatarUrl || ''}
alt=""

View File

@ -0,0 +1,58 @@
import _ from 'lodash'
import { Contract } from '../../../common/contract'
import { getPayouts } from '../../../common/payouts'
import { Bet } from './bets'
export function scoreCreators(contracts: Contract[], bets: Bet[][]) {
const creatorScore = _.mapValues(
_.groupBy(contracts, ({ creatorId }) => creatorId),
(contracts) => _.sumBy(contracts, ({ pool }) => pool.YES + pool.NO)
)
return creatorScore
}
export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
const userScoresByContract = contracts.map((contract, index) =>
scoreUsersByContract(contract, bets[index])
)
const userScores: { [userId: string]: number } = {}
for (const scores of userScoresByContract) {
for (const [userId, score] of Object.entries(scores)) {
if (userScores[userId] === undefined) userScores[userId] = 0
userScores[userId] += score
}
}
return userScores
}
function scoreUsersByContract(contract: Contract, bets: Bet[]) {
const { resolution } = contract
const [closedBets, openBets] = _.partition(
bets,
(bet) => bet.isSold || bet.sale
)
const resolvePayouts = getPayouts(resolution ?? 'MKT', contract, openBets)
const salePayouts = closedBets.map((bet) => {
const { userId, sale } = bet
return { userId, payout: sale ? sale.amount : 0 }
})
const investments = bets
.filter((bet) => !bet.sale)
.map((bet) => {
const { userId, amount } = bet
return { userId, payout: -amount }
})
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
const userScore = _.mapValues(
_.groupBy(netPayouts, (payout) => payout.userId),
(payouts) => _.sumBy(payouts, ({ payout }) => payout)
)
return userScore
}

View File

@ -6,19 +6,36 @@ import { Leaderboard } from '../../../components/leaderboard'
import { Page } from '../../../components/page'
import { SiteLink } from '../../../components/site-link'
import { formatMoney } from '../../../lib/util/format'
import { foldPath, getFoldBySlug } from '../../../lib/firebase/folds'
import {
foldPath,
getFoldBySlug,
getFoldContracts,
} from '../../../lib/firebase/folds'
import { Fold } from '../../../../common/fold'
import { Spacer } from '../../../components/layout/spacer'
import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring'
import { getUser, User } from '../../../lib/firebase/users'
import { listAllBets } from '../../../lib/firebase/bets'
export async function getStaticProps(props: { params: { foldSlug: string } }) {
const { foldSlug } = props.params
const fold = await getFoldBySlug(foldSlug)
const contracts = fold ? await getFoldContracts(fold) : []
const bets = await Promise.all(
contracts.map((contract) => listAllBets(contract.id))
)
const creatorScores = scoreCreators(contracts, bets)
const [topCreators, topCreatorScores] = await toUserScores(creatorScores)
const traderScores = scoreTraders(contracts, bets)
const [topTraders, topTraderScores] = await toUserScores(traderScores)
return {
props: { fold },
props: { fold, topTraders, topTraderScores, topCreators, topCreatorScores },
revalidate: 60, // regenerate after a minute
revalidate: 15 * 60, // regenerate after 15 minutes
}
}
@ -26,8 +43,27 @@ export async function getStaticPaths() {
return { paths: [], fallback: 'blocking' }
}
export default function Leaderboards(props: { fold: Fold }) {
const { fold } = props
async function toUserScores(userScores: { [userId: string]: number }) {
const topUserPairs = _.take(
_.sortBy(Object.entries(userScores), ([_, score]) => -1 * score),
10
)
const topUsers = await Promise.all(
topUserPairs.map(([userId]) => getUser(userId))
)
const topUserScores = topUserPairs.map(([_, score]) => score)
return [topUsers, topUserScores] as const
}
export default function Leaderboards(props: {
fold: Fold
topTraders: User[]
topTraderScores: number[]
topCreators: User[]
topCreatorScores: number[]
}) {
const { fold, topTraders, topTraderScores, topCreators, topCreatorScores } =
props
return (
<Page>
<SiteLink href={foldPath(fold)}>
@ -37,24 +73,26 @@ export default function Leaderboards(props: { fold: Fold }) {
<Spacer h={4} />
<Col className="items-center lg:flex-row gap-10">
<Col className="lg:flex-row gap-10">
<Leaderboard
title="🏅 Top traders"
users={[]}
users={topTraders}
columns={[
{
header: 'Total profit',
renderCell: (user) => formatMoney(user.totalPnLCached),
renderCell: (user) =>
formatMoney(topTraderScores[topTraders.indexOf(user)]),
},
]}
/>
<Leaderboard
title="🏅 Top creators"
users={[]}
users={topCreators}
columns={[
{
header: 'Market volume',
renderCell: (user) => formatMoney(user.creatorVolumeCached),
header: 'Market pool',
renderCell: (user) =>
formatMoney(topCreatorScores[topCreators.indexOf(user)]),
},
]}
/>