Implement leaderboards for folds!
This commit is contained in:
parent
bc1decdbfc
commit
aa1022546d
|
@ -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'
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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=""
|
||||
|
|
58
web/lib/firebase/scoring.ts
Normal file
58
web/lib/firebase/scoring.ts
Normal 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
|
||||
}
|
|
@ -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)]),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue
Block a user