Sell bets (#12)
* sell bet * dev mode * single-pot no-refund payoff; bet selling * Increase default fetch size 25 -> 99 * Fix about page numbering * Don't flash no markets when loading on tag page. * Change Title to use body font * Make a bunch of predictions at once (#9) * Set up a page to make bulk predictions * Integrate preview into the same card * List created predictions * Make changes per James's comments * Increase the starting balance (#11) * Remove references to paying for our Mantic Dollars * Update simulator to use new calculations * Change simulator random to be evenly random again * Sell bet UI * Migrate contracts and bets script * Add comment to script * bets => trades; exclude sold bets * change sale formula * Change current value to uncapped sell value. * Disable sell button while selling * Update some 'bet' to 'trade' Co-authored-by: Austin Chen <akrolsmir@gmail.com> Co-authored-by: jahooma <jahooma@gmail.com>
This commit is contained in:
parent
856a2453a1
commit
f48ae0170b
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"projects": {
|
"projects": {
|
||||||
"default": "mantic-markets"
|
"default": "mantic-markets",
|
||||||
|
"prod": "mantic-markets",
|
||||||
|
"dev": "dev-mantic-markets"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "functions",
|
"name": "functions",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
"watch": "tsc -w",
|
||||||
"serve": "yarn build && firebase emulators:start --only functions",
|
"serve": "yarn build && firebase emulators:start --only functions",
|
||||||
"shell": "yarn build && firebase functions:shell",
|
"shell": "yarn build && firebase functions:shell",
|
||||||
"start": "yarn shell",
|
"start": "yarn shell",
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
admin.initializeApp()
|
admin.initializeApp()
|
||||||
|
|
||||||
export * from './keep-awake'
|
// export * from './keep-awake'
|
||||||
export * from './place-bet'
|
export * from './place-bet'
|
||||||
export * from './resolve-market'
|
export * from './resolve-market'
|
||||||
|
export * from './sell-bet'
|
||||||
|
|
|
@ -43,7 +43,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
.collection(`contracts/${contractId}/bets`)
|
.collection(`contracts/${contractId}/bets`)
|
||||||
.doc()
|
.doc()
|
||||||
|
|
||||||
const { newBet, newPool, newDpmWeights, newBalance } = getNewBetInfo(
|
const { newBet, newPool, newTotalShares, newBalance } = getNewBetInfo(
|
||||||
user,
|
user,
|
||||||
outcome,
|
outcome,
|
||||||
amount,
|
amount,
|
||||||
|
@ -54,7 +54,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
transaction.create(newBetDoc, newBet)
|
transaction.create(newBetDoc, newBet)
|
||||||
transaction.update(contractDoc, {
|
transaction.update(contractDoc, {
|
||||||
pool: newPool,
|
pool: newPool,
|
||||||
dpmWeights: newDpmWeights,
|
totalShares: newTotalShares,
|
||||||
})
|
})
|
||||||
transaction.update(userDoc, { balance: newBalance })
|
transaction.update(userDoc, { balance: newBalance })
|
||||||
|
|
||||||
|
@ -79,29 +79,19 @@ const getNewBetInfo = (
|
||||||
? { YES: yesPool + amount, NO: noPool }
|
? { YES: yesPool + amount, NO: noPool }
|
||||||
: { YES: yesPool, NO: noPool + amount }
|
: { YES: yesPool, NO: noPool + amount }
|
||||||
|
|
||||||
const dpmWeight =
|
const shares =
|
||||||
outcome === 'YES'
|
outcome === 'YES'
|
||||||
? (amount * noPool ** 2) / (yesPool ** 2 + amount * yesPool)
|
? amount + (amount * noPool ** 2) / (yesPool ** 2 + amount * yesPool)
|
||||||
: (amount * yesPool ** 2) / (noPool ** 2 + amount * noPool)
|
: amount + (amount * yesPool ** 2) / (noPool ** 2 + amount * noPool)
|
||||||
|
|
||||||
const { YES: yesWeight, NO: noWeight } = contract.dpmWeights || {
|
const { YES: yesShares, NO: noShares } = contract.totalShares
|
||||||
YES: 0,
|
|
||||||
NO: 0,
|
|
||||||
} // only nesc for old contracts
|
|
||||||
|
|
||||||
const newDpmWeights =
|
const newTotalShares =
|
||||||
outcome === 'YES'
|
outcome === 'YES'
|
||||||
? { YES: yesWeight + dpmWeight, NO: noWeight }
|
? { YES: yesShares + shares, NO: noShares }
|
||||||
: { YES: yesWeight, NO: noWeight + dpmWeight }
|
: { YES: yesShares, NO: noShares + shares }
|
||||||
|
|
||||||
const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2)
|
const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2)
|
||||||
|
|
||||||
const probAverage =
|
|
||||||
(amount +
|
|
||||||
noPool * Math.atan(yesPool / noPool) -
|
|
||||||
noPool * Math.atan((amount + yesPool) / noPool)) /
|
|
||||||
amount
|
|
||||||
|
|
||||||
const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2)
|
const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2)
|
||||||
|
|
||||||
const newBet: Bet = {
|
const newBet: Bet = {
|
||||||
|
@ -109,15 +99,14 @@ const getNewBetInfo = (
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
dpmWeight,
|
shares,
|
||||||
outcome,
|
outcome,
|
||||||
probBefore,
|
probBefore,
|
||||||
probAverage,
|
|
||||||
probAfter,
|
probAfter,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance - amount
|
const newBalance = user.balance - amount
|
||||||
|
|
||||||
return { newBet, newPool, newDpmWeights, newBalance }
|
return { newBet, newPool, newTotalShares, newBalance }
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,22 +78,27 @@ export const resolveMarket = functions
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
const getPayouts = (outcome: string, contract: Contract, bets: Bet[]) => {
|
const getPayouts = (outcome: string, contract: Contract, bets: Bet[]) => {
|
||||||
const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES')
|
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
||||||
|
const [yesBets, noBets] = _.partition(
|
||||||
|
openBets,
|
||||||
|
(bet) => bet.outcome === 'YES'
|
||||||
|
)
|
||||||
|
|
||||||
const [pool, winningBets] =
|
const startPool = contract.startPool.YES + contract.startPool.NO
|
||||||
|
const truePool = contract.pool.YES + contract.pool.NO - startPool
|
||||||
|
|
||||||
|
const [totalShares, winningBets] =
|
||||||
outcome === 'YES'
|
outcome === 'YES'
|
||||||
? [contract.pool.NO - contract.startPool.NO, yesBets]
|
? [contract.totalShares.YES, yesBets]
|
||||||
: [contract.pool.YES - contract.startPool.YES, noBets]
|
: [contract.totalShares.NO, noBets]
|
||||||
|
|
||||||
const finalPool = (1 - PLATFORM_FEE - CREATOR_FEE) * pool
|
const finalPool = (1 - PLATFORM_FEE - CREATOR_FEE) * truePool
|
||||||
const creatorPayout = CREATOR_FEE * pool
|
const creatorPayout = CREATOR_FEE * truePool
|
||||||
console.log('final pool:', finalPool, 'creator fee:', creatorPayout)
|
console.log('final pool:', finalPool, 'creator fee:', creatorPayout)
|
||||||
|
|
||||||
const sumWeights = _.sumBy(winningBets, (bet) => bet.dpmWeight)
|
|
||||||
|
|
||||||
const winnerPayouts = winningBets.map((bet) => ({
|
const winnerPayouts = winningBets.map((bet) => ({
|
||||||
userId: bet.userId,
|
userId: bet.userId,
|
||||||
payout: bet.amount + (bet.dpmWeight / sumWeights) * finalPool,
|
payout: (bet.shares / totalShares) * finalPool,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return winnerPayouts.concat([
|
return winnerPayouts.concat([
|
||||||
|
|
58
functions/src/scripts/migrate-contract.ts
Normal file
58
functions/src/scripts/migrate-contract.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
import { Bet } from '../types/bet'
|
||||||
|
import { Contract } from '../types/contract'
|
||||||
|
|
||||||
|
type DocRef = admin.firestore.DocumentReference
|
||||||
|
|
||||||
|
// Generate your own private key, and set the path below:
|
||||||
|
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
|
||||||
|
const serviceAccount = require('../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json')
|
||||||
|
|
||||||
|
admin.initializeApp({
|
||||||
|
credential: admin.credential.cert(serviceAccount),
|
||||||
|
})
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
async function migrateBet(contractRef: DocRef, bet: Bet) {
|
||||||
|
const { dpmWeight, amount, id } = bet as Bet & { dpmWeight: number }
|
||||||
|
const shares = dpmWeight + amount
|
||||||
|
|
||||||
|
await contractRef.collection('bets').doc(id).update({ shares })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateContract(contractRef: DocRef, contract: Contract) {
|
||||||
|
const bets = await contractRef
|
||||||
|
.collection('bets')
|
||||||
|
.get()
|
||||||
|
.then((snap) => snap.docs.map((bet) => bet.data() as Bet))
|
||||||
|
|
||||||
|
const totalShares = {
|
||||||
|
YES: _.sumBy(bets, (bet) => (bet.outcome === 'YES' ? bet.shares : 0)),
|
||||||
|
NO: _.sumBy(bets, (bet) => (bet.outcome === 'NO' ? bet.shares : 0)),
|
||||||
|
}
|
||||||
|
|
||||||
|
await contractRef.update({ totalShares })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateContracts() {
|
||||||
|
console.log('Migrating contracts')
|
||||||
|
|
||||||
|
const snapshot = await firestore.collection('contracts').get()
|
||||||
|
const contracts = snapshot.docs.map((doc) => doc.data() as Contract)
|
||||||
|
|
||||||
|
console.log('Loaded contracts', contracts.length)
|
||||||
|
|
||||||
|
for (const contract of contracts) {
|
||||||
|
const contractRef = firestore.doc(`contracts/${contract.id}`)
|
||||||
|
const betsSnapshot = await contractRef.collection('bets').get()
|
||||||
|
const bets = betsSnapshot.docs.map((bet) => bet.data() as Bet)
|
||||||
|
|
||||||
|
console.log('contract', contract.question, 'bets', bets.length)
|
||||||
|
|
||||||
|
for (const bet of bets) await migrateBet(contractRef, bet)
|
||||||
|
await migrateContract(contractRef, contract)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) migrateContracts().then(() => process.exit())
|
162
functions/src/sell-bet.ts
Normal file
162
functions/src/sell-bet.ts
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
|
||||||
|
import { CREATOR_FEE, PLATFORM_FEE } from './resolve-market'
|
||||||
|
import { Bet } from './types/bet'
|
||||||
|
import { Contract } from './types/contract'
|
||||||
|
import { User } from './types/user'
|
||||||
|
|
||||||
|
export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
|
async (
|
||||||
|
data: {
|
||||||
|
contractId: string
|
||||||
|
betId: string
|
||||||
|
},
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const userId = context?.auth?.uid
|
||||||
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||||
|
|
||||||
|
const { contractId, betId } = data
|
||||||
|
|
||||||
|
// run as transaction to prevent race conditions
|
||||||
|
return await firestore.runTransaction(async (transaction) => {
|
||||||
|
const userDoc = firestore.doc(`users/${userId}`)
|
||||||
|
const userSnap = await transaction.get(userDoc)
|
||||||
|
if (!userSnap.exists)
|
||||||
|
return { status: 'error', message: 'User not found' }
|
||||||
|
const user = userSnap.data() as User
|
||||||
|
|
||||||
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
|
const contractSnap = await transaction.get(contractDoc)
|
||||||
|
if (!contractSnap.exists)
|
||||||
|
return { status: 'error', message: 'Invalid contract' }
|
||||||
|
const contract = contractSnap.data() as Contract
|
||||||
|
|
||||||
|
const betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`)
|
||||||
|
const betSnap = await transaction.get(betDoc)
|
||||||
|
if (!betSnap.exists) return { status: 'error', message: 'Invalid bet' }
|
||||||
|
const bet = betSnap.data() as Bet
|
||||||
|
|
||||||
|
if (bet.isSold) return { status: 'error', message: 'Bet already sold' }
|
||||||
|
|
||||||
|
const newBetDoc = firestore
|
||||||
|
.collection(`contracts/${contractId}/bets`)
|
||||||
|
.doc()
|
||||||
|
|
||||||
|
const { newBet, newPool, newTotalShares, newBalance, creatorFee } =
|
||||||
|
getSellBetInfo(user, bet, contract, newBetDoc.id)
|
||||||
|
|
||||||
|
const creatorDoc = firestore.doc(`users/${contract.creatorId}`)
|
||||||
|
const creatorSnap = await transaction.get(creatorDoc)
|
||||||
|
if (creatorSnap.exists) {
|
||||||
|
const creator = creatorSnap.data() as User
|
||||||
|
const creatorNewBalance = creator.balance + creatorFee
|
||||||
|
transaction.update(creatorDoc, { balance: creatorNewBalance })
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.update(betDoc, { isSold: true })
|
||||||
|
transaction.create(newBetDoc, newBet)
|
||||||
|
transaction.update(contractDoc, {
|
||||||
|
pool: newPool,
|
||||||
|
totalShares: newTotalShares,
|
||||||
|
})
|
||||||
|
transaction.update(userDoc, { balance: newBalance })
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
const getSellBetInfo = (
|
||||||
|
user: User,
|
||||||
|
bet: Bet,
|
||||||
|
contract: Contract,
|
||||||
|
newBetId: string
|
||||||
|
) => {
|
||||||
|
const { id: betId, amount, shares, outcome } = bet
|
||||||
|
|
||||||
|
const { YES: yesPool, NO: noPool } = contract.pool
|
||||||
|
const { YES: yesStart, NO: noStart } = contract.startPool
|
||||||
|
const { YES: yesShares, NO: noShares } = contract.totalShares
|
||||||
|
|
||||||
|
const [y, n, s] = [yesPool, noPool, shares]
|
||||||
|
|
||||||
|
const shareValue =
|
||||||
|
outcome === 'YES'
|
||||||
|
? // https://www.wolframalpha.com/input/?i=b+%2B+%28b+n%5E2%29%2F%28y+%28-b+%2B+y%29%29+%3D+c+solve+b
|
||||||
|
(n ** 2 +
|
||||||
|
s * y +
|
||||||
|
y ** 2 -
|
||||||
|
Math.sqrt(
|
||||||
|
n ** 4 + (s - y) ** 2 * y ** 2 + 2 * n ** 2 * y * (s + y)
|
||||||
|
)) /
|
||||||
|
(2 * y)
|
||||||
|
: (y ** 2 +
|
||||||
|
s * n +
|
||||||
|
n ** 2 -
|
||||||
|
Math.sqrt(
|
||||||
|
y ** 4 + (s - n) ** 2 * n ** 2 + 2 * y ** 2 * n * (s + n)
|
||||||
|
)) /
|
||||||
|
(2 * n)
|
||||||
|
|
||||||
|
const startPool = yesStart + noStart
|
||||||
|
const pool = yesPool + noPool - startPool
|
||||||
|
|
||||||
|
const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2)
|
||||||
|
|
||||||
|
const f = pool / (probBefore * yesShares + (1 - probBefore) * noShares)
|
||||||
|
|
||||||
|
const myPool = outcome === 'YES' ? yesPool - yesStart : noPool - noStart
|
||||||
|
|
||||||
|
const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool)
|
||||||
|
|
||||||
|
const newPool =
|
||||||
|
outcome === 'YES'
|
||||||
|
? { YES: yesPool - adjShareValue, NO: noPool }
|
||||||
|
: { YES: yesPool, NO: noPool - adjShareValue }
|
||||||
|
|
||||||
|
const newTotalShares =
|
||||||
|
outcome === 'YES'
|
||||||
|
? { YES: yesShares - shares, NO: noShares }
|
||||||
|
: { YES: yesShares, NO: noShares - shares }
|
||||||
|
|
||||||
|
const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2)
|
||||||
|
|
||||||
|
const creatorFee = CREATOR_FEE * adjShareValue
|
||||||
|
const saleAmount = (1 - CREATOR_FEE - PLATFORM_FEE) * adjShareValue
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'SELL M$',
|
||||||
|
amount,
|
||||||
|
outcome,
|
||||||
|
'for M$',
|
||||||
|
saleAmount,
|
||||||
|
'M$/share:',
|
||||||
|
f,
|
||||||
|
'creator fee: M$',
|
||||||
|
creatorFee
|
||||||
|
)
|
||||||
|
|
||||||
|
const newBet: Bet = {
|
||||||
|
id: newBetId,
|
||||||
|
userId: user.id,
|
||||||
|
contractId: contract.id,
|
||||||
|
amount: -adjShareValue,
|
||||||
|
shares: -shares,
|
||||||
|
outcome,
|
||||||
|
probBefore,
|
||||||
|
probAfter,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
sale: {
|
||||||
|
amount: saleAmount,
|
||||||
|
betId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBalance = user.balance + saleAmount
|
||||||
|
|
||||||
|
return { newBet, newPool, newTotalShares, newBalance, creatorFee }
|
||||||
|
}
|
|
@ -2,11 +2,20 @@ export type Bet = {
|
||||||
id: string
|
id: string
|
||||||
userId: string
|
userId: string
|
||||||
contractId: string
|
contractId: string
|
||||||
amount: number // Amount of bet
|
|
||||||
outcome: 'YES' | 'NO' // Chosen outcome
|
amount: number // bet size; negative if SELL bet
|
||||||
createdTime: number
|
outcome: 'YES' | 'NO'
|
||||||
|
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
||||||
|
|
||||||
probBefore: number
|
probBefore: number
|
||||||
probAverage: number
|
|
||||||
probAfter: number
|
probAfter: number
|
||||||
dpmWeight: number // Dynamic Parimutuel weight
|
|
||||||
|
sale?: {
|
||||||
|
amount: number // amount user makes from sale
|
||||||
|
betId: string // id of bet being sold
|
||||||
|
}
|
||||||
|
|
||||||
|
isSold?: boolean // true if this BUY bet has been sold
|
||||||
|
|
||||||
|
createdTime: number
|
||||||
}
|
}
|
|
@ -12,7 +12,7 @@ export type Contract = {
|
||||||
|
|
||||||
startPool: { YES: number; NO: number }
|
startPool: { YES: number; NO: number }
|
||||||
pool: { YES: number; NO: number }
|
pool: { YES: number; NO: number }
|
||||||
dpmWeights: { YES: number; NO: number }
|
totalShares: { YES: number; NO: number }
|
||||||
|
|
||||||
createdTime: number // Milliseconds since epoch
|
createdTime: number // Milliseconds since epoch
|
||||||
lastUpdatedTime: number // If the question or description was changed
|
lastUpdatedTime: number // If the question or description was changed
|
||||||
|
|
|
@ -12,9 +12,9 @@ import { formatMoney, formatPercent } from '../lib/util/format'
|
||||||
import { Title } from './title'
|
import { Title } from './title'
|
||||||
import {
|
import {
|
||||||
getProbability,
|
getProbability,
|
||||||
getDpmWeight,
|
calculateShares,
|
||||||
getProbabilityAfterBet,
|
getProbabilityAfterBet,
|
||||||
} from '../lib/calculation/contract'
|
} from '../lib/calculate'
|
||||||
import { firebaseLogin } from '../lib/firebase/users'
|
import { firebaseLogin } from '../lib/firebase/users'
|
||||||
|
|
||||||
export function BetPanel(props: { contract: Contract; className?: string }) {
|
export function BetPanel(props: { contract: Contract; className?: string }) {
|
||||||
|
@ -84,9 +84,9 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
||||||
betChoice,
|
betChoice,
|
||||||
betAmount ?? 0
|
betAmount ?? 0
|
||||||
)
|
)
|
||||||
const dpmWeight = getDpmWeight(contract.pool, betAmount ?? 0, betChoice)
|
const shares = calculateShares(contract.pool, betAmount ?? 0, betChoice)
|
||||||
|
|
||||||
const estimatedWinnings = Math.floor((betAmount ?? 0) + dpmWeight)
|
const estimatedWinnings = Math.floor(shares)
|
||||||
const estimatedReturn = betAmount
|
const estimatedReturn = betAmount
|
||||||
? (estimatedWinnings - betAmount) / betAmount
|
? (estimatedWinnings - betAmount) / betAmount
|
||||||
: 0
|
: 0
|
||||||
|
@ -98,7 +98,7 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
||||||
<Col
|
<Col
|
||||||
className={clsx('bg-gray-100 shadow-xl px-8 py-6 rounded-md', className)}
|
className={clsx('bg-gray-100 shadow-xl px-8 py-6 rounded-md', className)}
|
||||||
>
|
>
|
||||||
<Title className="!mt-0 whitespace-nowrap" text="Place a bet" />
|
<Title className="!mt-0 whitespace-nowrap" text="Place a trade" />
|
||||||
|
|
||||||
<div className="mt-2 mb-1 text-sm text-gray-400">Outcome</div>
|
<div className="mt-2 mb-1 text-sm text-gray-400">Outcome</div>
|
||||||
<YesNoSelector
|
<YesNoSelector
|
||||||
|
@ -107,7 +107,7 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
||||||
onSelect={(choice) => onBetChoice(choice)}
|
onSelect={(choice) => onBetChoice(choice)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-3 mb-1 text-sm text-gray-400">Bet amount</div>
|
<div className="mt-3 mb-1 text-sm text-gray-400">Amount</div>
|
||||||
<Col className="my-2">
|
<Col className="my-2">
|
||||||
<label className="input-group">
|
<label className="input-group">
|
||||||
<span className="text-sm bg-gray-200">M$</span>
|
<span className="text-sm bg-gray-200">M$</span>
|
||||||
|
@ -168,18 +168,18 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
||||||
)}
|
)}
|
||||||
onClick={betDisabled ? undefined : submitBet}
|
onClick={betDisabled ? undefined : submitBet}
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Submitting...' : 'Place bet'}
|
{isSubmitting ? 'Submitting...' : 'Submit trade'}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className="btn mt-4 border-none normal-case text-lg font-medium px-10 bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600"
|
className="btn mt-4 border-none normal-case text-lg font-medium px-10 bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600"
|
||||||
onClick={firebaseLogin}
|
onClick={firebaseLogin}
|
||||||
>
|
>
|
||||||
Sign in to bet!
|
Sign in to trade!
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{wasSubmitted && <div className="mt-4">Bet submitted!</div>}
|
{wasSubmitted && <div className="mt-4">Trade submitted!</div>}
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,13 @@ import { Row } from './layout/row'
|
||||||
import { UserLink } from './user-page'
|
import { UserLink } from './user-page'
|
||||||
import {
|
import {
|
||||||
calculatePayout,
|
calculatePayout,
|
||||||
|
calculateSaleAmount,
|
||||||
currentValue,
|
currentValue,
|
||||||
resolvedPayout,
|
resolvedPayout,
|
||||||
} from '../lib/calculation/contract'
|
} from '../lib/calculate'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { cloudFunction } from '../lib/firebase/api-call'
|
||||||
|
import { ConfirmationButton } from './confirmation-button'
|
||||||
|
|
||||||
export function BetsList(props: { user: User }) {
|
export function BetsList(props: { user: User }) {
|
||||||
const { user } = props
|
const { user } = props
|
||||||
|
@ -65,19 +68,26 @@ export function BetsList(props: { user: User }) {
|
||||||
contracts,
|
contracts,
|
||||||
(contract) => contract.isResolved
|
(contract) => contract.isResolved
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentBets = _.sumBy(unresolved, (contract) =>
|
const currentBets = _.sumBy(unresolved, (contract) =>
|
||||||
_.sumBy(contractBets[contract.id], (bet) => bet.amount)
|
_.sumBy(contractBets[contract.id], (bet) => {
|
||||||
|
if (bet.isSold || bet.sale) return 0
|
||||||
|
return bet.amount
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentBetsValue = _.sumBy(unresolved, (contract) =>
|
const currentBetsValue = _.sumBy(unresolved, (contract) =>
|
||||||
_.sumBy(contractBets[contract.id], (bet) => currentValue(contract, bet))
|
_.sumBy(contractBets[contract.id], (bet) => {
|
||||||
|
if (bet.isSold || bet.sale) return 0
|
||||||
|
return currentValue(contract, bet)
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="mt-6 gap-6">
|
<Col className="mt-6 gap-6">
|
||||||
<Row className="gap-8">
|
<Row className="gap-8">
|
||||||
<Col>
|
<Col>
|
||||||
<div className="text-sm text-gray-500">Active bets</div>
|
<div className="text-sm text-gray-500">Currently invested</div>
|
||||||
<div>{formatMoney(currentBets)}</div>
|
<div>{formatMoney(currentBets)}</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
|
@ -173,16 +183,17 @@ export function MyBetsSummary(props: {
|
||||||
const { bets, contract, className } = props
|
const { bets, contract, className } = props
|
||||||
const { resolution } = contract
|
const { resolution } = contract
|
||||||
|
|
||||||
const betsTotal = _.sumBy(bets, (bet) => bet.amount)
|
const excludeSales = bets.filter((b) => !b.isSold && !b.sale)
|
||||||
|
const betsTotal = _.sumBy(excludeSales, (bet) => bet.amount)
|
||||||
|
|
||||||
const betsPayout = resolution
|
const betsPayout = resolution
|
||||||
? _.sumBy(bets, (bet) => resolvedPayout(contract, bet))
|
? _.sumBy(bets, (bet) => resolvedPayout(contract, bet))
|
||||||
: 0
|
: 0
|
||||||
|
|
||||||
const yesWinnings = _.sumBy(bets, (bet) =>
|
const yesWinnings = _.sumBy(excludeSales, (bet) =>
|
||||||
calculatePayout(contract, bet, 'YES')
|
calculatePayout(contract, bet, 'YES')
|
||||||
)
|
)
|
||||||
const noWinnings = _.sumBy(bets, (bet) =>
|
const noWinnings = _.sumBy(excludeSales, (bet) =>
|
||||||
calculatePayout(contract, bet, 'NO')
|
calculatePayout(contract, bet, 'NO')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -190,7 +201,7 @@ export function MyBetsSummary(props: {
|
||||||
<Row className={clsx('gap-4 sm:gap-6', className)}>
|
<Row className={clsx('gap-4 sm:gap-6', className)}>
|
||||||
<Col>
|
<Col>
|
||||||
<div className="text-sm text-gray-500 whitespace-nowrap">
|
<div className="text-sm text-gray-500 whitespace-nowrap">
|
||||||
Total bets
|
Amount invested
|
||||||
</div>
|
</div>
|
||||||
<div className="whitespace-nowrap">{formatMoney(betsTotal)}</div>
|
<div className="whitespace-nowrap">{formatMoney(betsTotal)}</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -228,6 +239,11 @@ export function ContractBetsTable(props: {
|
||||||
}) {
|
}) {
|
||||||
const { contract, bets, className } = props
|
const { contract, bets, className } = props
|
||||||
|
|
||||||
|
const [sales, buys] = _.partition(bets, (bet) => bet.sale)
|
||||||
|
const salesDict = _.fromPairs(
|
||||||
|
sales.map((sale) => [sale.sale?.betId ?? '', sale])
|
||||||
|
)
|
||||||
|
|
||||||
const { isResolved } = contract
|
const { isResolved } = contract
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -237,15 +253,21 @@ export function ContractBetsTable(props: {
|
||||||
<tr className="p-2">
|
<tr className="p-2">
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th>Outcome</th>
|
<th>Outcome</th>
|
||||||
<th>Bet</th>
|
<th>Amount</th>
|
||||||
<th>Probability</th>
|
<th>Probability</th>
|
||||||
{!isResolved && <th>Est. max payout</th>}
|
{!isResolved && <th>Est. max payout</th>}
|
||||||
<th>{isResolved ? <>Payout</> : <>Current value</>}</th>
|
<th>{isResolved ? <>Payout</> : <>Current value</>}</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{bets.map((bet) => (
|
{buys.map((bet) => (
|
||||||
<BetRow key={bet.id} bet={bet} contract={contract} />
|
<BetRow
|
||||||
|
key={bet.id}
|
||||||
|
bet={bet}
|
||||||
|
sale={salesDict[bet.id]}
|
||||||
|
contract={contract}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -253,14 +275,22 @@ export function ContractBetsTable(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BetRow(props: { bet: Bet; contract: Contract }) {
|
function BetRow(props: { bet: Bet; contract: Contract; sale?: Bet }) {
|
||||||
const { bet, contract } = props
|
const { bet, sale, contract } = props
|
||||||
const { amount, outcome, createdTime, probBefore, probAfter, dpmWeight } = bet
|
const {
|
||||||
|
amount,
|
||||||
|
outcome,
|
||||||
|
createdTime,
|
||||||
|
probBefore,
|
||||||
|
probAfter,
|
||||||
|
shares,
|
||||||
|
isSold,
|
||||||
|
} = bet
|
||||||
const { isResolved } = contract
|
const { isResolved } = contract
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{dayjs(createdTime).format('MMM D, H:mma')}</td>
|
<td>{dayjs(createdTime).format('MMM D, h:mma')}</td>
|
||||||
<td>
|
<td>
|
||||||
<OutcomeLabel outcome={outcome} />
|
<OutcomeLabel outcome={outcome} />
|
||||||
</td>
|
</td>
|
||||||
|
@ -268,18 +298,63 @@ function BetRow(props: { bet: Bet; contract: Contract }) {
|
||||||
<td>
|
<td>
|
||||||
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
||||||
</td>
|
</td>
|
||||||
{!isResolved && <td>{formatMoney(amount + dpmWeight)}</td>}
|
{!isResolved && <td>{formatMoney(shares)}</td>}
|
||||||
<td>
|
<td>
|
||||||
{formatMoney(
|
{bet.isSold
|
||||||
|
? 'N/A'
|
||||||
|
: formatMoney(
|
||||||
isResolved
|
isResolved
|
||||||
? resolvedPayout(contract, bet)
|
? resolvedPayout(contract, bet)
|
||||||
|
: bet.sale
|
||||||
|
? bet.sale.amount ?? 0
|
||||||
: currentValue(contract, bet)
|
: currentValue(contract, bet)
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{sale ? (
|
||||||
|
<td>SOLD for {formatMoney(Math.abs(sale.amount))}</td>
|
||||||
|
) : (
|
||||||
|
!isResolved &&
|
||||||
|
!isSold && (
|
||||||
|
<td className="text-neutral">
|
||||||
|
<SellButton contract={contract} bet={bet} />
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sellBet = cloudFunction('sellBet')
|
||||||
|
|
||||||
|
function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
|
const { contract, bet } = props
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmationButton
|
||||||
|
id={`sell-${bet.id}`}
|
||||||
|
openModelBtn={{
|
||||||
|
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
||||||
|
label: 'Sell',
|
||||||
|
}}
|
||||||
|
submitBtn={{ className: 'btn-primary' }}
|
||||||
|
onSubmit={async () => {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
await sellBet({ contractId: contract.id, betId: bet.id })
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-2xl mb-4">Sell</div>
|
||||||
|
<div>
|
||||||
|
Do you want to sell your {formatMoney(bet.amount)} position on{' '}
|
||||||
|
<OutcomeLabel outcome={bet.outcome} /> for{' '}
|
||||||
|
{formatMoney(calculateSaleAmount(contract, bet))}?
|
||||||
|
</div>
|
||||||
|
</ConfirmationButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function OutcomeLabel(props: { outcome: 'YES' | 'NO' | 'CANCEL' }) {
|
function OutcomeLabel(props: { outcome: 'YES' | 'NO' | 'CANCEL' }) {
|
||||||
const { outcome } = props
|
const { outcome } = props
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ export const ContractOverview = (props: {
|
||||||
}) => {
|
}) => {
|
||||||
const { contract, className } = props
|
const { contract, className } = props
|
||||||
const { resolution, creatorId } = contract
|
const { resolution, creatorId } = contract
|
||||||
const { probPercent, volume } = compute(contract)
|
const { probPercent, truePool } = compute(contract)
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const isCreator = user?.id === creatorId
|
const isCreator = user?.id === creatorId
|
||||||
|
@ -140,7 +140,7 @@ export const ContractOverview = (props: {
|
||||||
<ContractDescription contract={contract} isCreator={isCreator} />
|
<ContractDescription contract={contract} isCreator={isCreator} />
|
||||||
|
|
||||||
{/* Show a delete button for contracts without any trading */}
|
{/* Show a delete button for contracts without any trading */}
|
||||||
{isCreator && volume === 0 && (
|
{isCreator && truePool === 0 && (
|
||||||
<>
|
<>
|
||||||
<Spacer h={8} />
|
<Spacer h={8} />
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { Linkify } from './linkify'
|
||||||
|
|
||||||
export function ContractDetails(props: { contract: Contract }) {
|
export function ContractDetails(props: { contract: Contract }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
const { volume, createdDate, resolvedDate } = compute(contract)
|
const { truePool, createdDate, resolvedDate } = compute(contract)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="flex-wrap text-sm text-gray-500">
|
<Row className="flex-wrap text-sm text-gray-500">
|
||||||
|
@ -29,7 +29,7 @@ export function ContractDetails(props: { contract: Contract }) {
|
||||||
{resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}
|
{resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-2">•</div>
|
<div className="mx-2">•</div>
|
||||||
<div className="whitespace-nowrap">{formatMoney(volume)} volume</div>
|
<div className="whitespace-nowrap">{formatMoney(truePool)} pool</div>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -110,14 +110,14 @@ function ContractsGrid(props: { contracts: Contract[] }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Sort = 'createdTime' | 'volume' | 'resolved' | 'all'
|
type Sort = 'createdTime' | 'pool' | 'resolved' | 'all'
|
||||||
export function SearchableGrid(props: {
|
export function SearchableGrid(props: {
|
||||||
contracts: Contract[]
|
contracts: Contract[]
|
||||||
defaultSort?: Sort
|
defaultSort?: Sort
|
||||||
}) {
|
}) {
|
||||||
const { contracts, defaultSort } = props
|
const { contracts, defaultSort } = props
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [sort, setSort] = useState(defaultSort || 'volume')
|
const [sort, setSort] = useState(defaultSort || 'pool')
|
||||||
|
|
||||||
function check(corpus: String) {
|
function check(corpus: String) {
|
||||||
return corpus.toLowerCase().includes(query.toLowerCase())
|
return corpus.toLowerCase().includes(query.toLowerCase())
|
||||||
|
@ -132,8 +132,8 @@ export function SearchableGrid(props: {
|
||||||
|
|
||||||
if (sort === 'createdTime' || sort === 'resolved' || sort === 'all') {
|
if (sort === 'createdTime' || sort === 'resolved' || sort === 'all') {
|
||||||
matches.sort((a, b) => b.createdTime - a.createdTime)
|
matches.sort((a, b) => b.createdTime - a.createdTime)
|
||||||
} else if (sort === 'volume') {
|
} else if (sort === 'pool') {
|
||||||
matches.sort((a, b) => compute(b).volume - compute(a).volume)
|
matches.sort((a, b) => compute(b).truePool - compute(a).truePool)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sort !== 'all') {
|
if (sort !== 'all') {
|
||||||
|
@ -159,7 +159,7 @@ export function SearchableGrid(props: {
|
||||||
value={sort}
|
value={sort}
|
||||||
onChange={(e) => setSort(e.target.value as Sort)}
|
onChange={(e) => setSort(e.target.value as Sort)}
|
||||||
>
|
>
|
||||||
<option value="volume">Most traded</option>
|
<option value="pool">Most traded</option>
|
||||||
<option value="createdTime">Newest first</option>
|
<option value="createdTime">Newest first</option>
|
||||||
<option value="resolved">Resolved</option>
|
<option value="resolved">Resolved</option>
|
||||||
<option value="all">All markets</option>
|
<option value="all">All markets</option>
|
||||||
|
|
|
@ -41,8 +41,8 @@ function getNavigationOptions(user: User, options: { mobile: boolean }) {
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
name: 'Your bets',
|
name: 'Your trades',
|
||||||
href: '/bets',
|
href: '/trades',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Your markets',
|
name: 'Your markets',
|
||||||
|
@ -64,7 +64,7 @@ function ProfileSummary(props: { user: User }) {
|
||||||
<div className="rounded-full w-10 h-10 mr-4">
|
<div className="rounded-full w-10 h-10 mr-4">
|
||||||
<Image src={user.avatarUrl} width={40} height={40} />
|
<Image src={user.avatarUrl} width={40} height={40} />
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate" style={{ maxWidth: 175 }}>
|
<div className="truncate text-left" style={{ maxWidth: 175 }}>
|
||||||
{user.name}
|
{user.name}
|
||||||
<div className="text-gray-700 text-sm">{formatMoney(user.balance)}</div>
|
<div className="text-gray-700 text-sm">{formatMoney(user.balance)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
134
web/lib/calculate.ts
Normal file
134
web/lib/calculate.ts
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import { Bet } from './firebase/bets'
|
||||||
|
import { Contract } from './firebase/contracts'
|
||||||
|
|
||||||
|
const fees = 0.02
|
||||||
|
|
||||||
|
export function getProbability(pool: { YES: number; NO: number }) {
|
||||||
|
const [yesPool, noPool] = [pool.YES, pool.NO]
|
||||||
|
const numerator = Math.pow(yesPool, 2)
|
||||||
|
const denominator = Math.pow(yesPool, 2) + Math.pow(noPool, 2)
|
||||||
|
return numerator / denominator
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProbabilityAfterBet(
|
||||||
|
pool: { YES: number; NO: number },
|
||||||
|
outcome: 'YES' | 'NO',
|
||||||
|
bet: number
|
||||||
|
) {
|
||||||
|
const [YES, NO] = [
|
||||||
|
pool.YES + (outcome === 'YES' ? bet : 0),
|
||||||
|
pool.NO + (outcome === 'NO' ? bet : 0),
|
||||||
|
]
|
||||||
|
return getProbability({ YES, NO })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateShares(
|
||||||
|
pool: { YES: number; NO: number },
|
||||||
|
bet: number,
|
||||||
|
betChoice: 'YES' | 'NO'
|
||||||
|
) {
|
||||||
|
const [yesPool, noPool] = [pool.YES, pool.NO]
|
||||||
|
|
||||||
|
return betChoice === 'YES'
|
||||||
|
? bet + (bet * noPool ** 2) / (yesPool ** 2 + bet * yesPool)
|
||||||
|
: bet + (bet * yesPool ** 2) / (noPool ** 2 + bet * noPool)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculatePayout(
|
||||||
|
contract: Contract,
|
||||||
|
bet: Bet,
|
||||||
|
outcome: 'YES' | 'NO' | 'CANCEL'
|
||||||
|
) {
|
||||||
|
const { amount, outcome: betOutcome, shares } = bet
|
||||||
|
|
||||||
|
if (outcome === 'CANCEL') return amount
|
||||||
|
if (betOutcome !== outcome) return 0
|
||||||
|
|
||||||
|
const { totalShares } = contract
|
||||||
|
|
||||||
|
if (totalShares[outcome] === 0) return 0
|
||||||
|
|
||||||
|
const startPool = contract.startPool.YES + contract.startPool.NO
|
||||||
|
const pool = contract.pool.YES + contract.pool.NO - startPool
|
||||||
|
|
||||||
|
return (1 - fees) * (shares / totalShares[outcome]) * pool
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvedPayout(contract: Contract, bet: Bet) {
|
||||||
|
if (contract.resolution)
|
||||||
|
return calculatePayout(contract, bet, contract.resolution)
|
||||||
|
throw new Error('Contract was not resolved')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentValue(contract: Contract, bet: Bet) {
|
||||||
|
// const prob = getProbability(contract.pool)
|
||||||
|
// const yesPayout = calculatePayout(contract, bet, 'YES')
|
||||||
|
// const noPayout = calculatePayout(contract, bet, 'NO')
|
||||||
|
|
||||||
|
// return prob * yesPayout + (1 - prob) * noPayout
|
||||||
|
|
||||||
|
const { shares, outcome } = bet
|
||||||
|
|
||||||
|
const { YES: yesPool, NO: noPool } = contract.pool
|
||||||
|
const [y, n, s] = [yesPool, noPool, shares]
|
||||||
|
|
||||||
|
const shareValue =
|
||||||
|
outcome === 'YES'
|
||||||
|
? // https://www.wolframalpha.com/input/?i=b+%2B+%28b+n%5E2%29%2F%28y+%28-b+%2B+y%29%29+%3D+c+solve+b
|
||||||
|
(n ** 2 +
|
||||||
|
s * y +
|
||||||
|
y ** 2 -
|
||||||
|
Math.sqrt(
|
||||||
|
n ** 4 + (s - y) ** 2 * y ** 2 + 2 * n ** 2 * y * (s + y)
|
||||||
|
)) /
|
||||||
|
(2 * y)
|
||||||
|
: (y ** 2 +
|
||||||
|
s * n +
|
||||||
|
n ** 2 -
|
||||||
|
Math.sqrt(
|
||||||
|
y ** 4 + (s - n) ** 2 * n ** 2 + 2 * y ** 2 * n * (s + n)
|
||||||
|
)) /
|
||||||
|
(2 * n)
|
||||||
|
|
||||||
|
return (1 - fees) * shareValue
|
||||||
|
}
|
||||||
|
export function calculateSaleAmount(contract: Contract, bet: Bet) {
|
||||||
|
const { shares, outcome } = bet
|
||||||
|
|
||||||
|
const { YES: yesPool, NO: noPool } = contract.pool
|
||||||
|
const { YES: yesStart, NO: noStart } = contract.startPool
|
||||||
|
const { YES: yesShares, NO: noShares } = contract.totalShares
|
||||||
|
|
||||||
|
const [y, n, s] = [yesPool, noPool, shares]
|
||||||
|
|
||||||
|
const shareValue =
|
||||||
|
outcome === 'YES'
|
||||||
|
? // https://www.wolframalpha.com/input/?i=b+%2B+%28b+n%5E2%29%2F%28y+%28-b+%2B+y%29%29+%3D+c+solve+b
|
||||||
|
(n ** 2 +
|
||||||
|
s * y +
|
||||||
|
y ** 2 -
|
||||||
|
Math.sqrt(
|
||||||
|
n ** 4 + (s - y) ** 2 * y ** 2 + 2 * n ** 2 * y * (s + y)
|
||||||
|
)) /
|
||||||
|
(2 * y)
|
||||||
|
: (y ** 2 +
|
||||||
|
s * n +
|
||||||
|
n ** 2 -
|
||||||
|
Math.sqrt(
|
||||||
|
y ** 4 + (s - n) ** 2 * n ** 2 + 2 * y ** 2 * n * (s + n)
|
||||||
|
)) /
|
||||||
|
(2 * n)
|
||||||
|
|
||||||
|
const startPool = yesStart + noStart
|
||||||
|
const pool = yesPool + noPool - startPool
|
||||||
|
|
||||||
|
const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2)
|
||||||
|
const f = pool / (probBefore * yesShares + (1 - probBefore) * noShares)
|
||||||
|
|
||||||
|
const myPool = outcome === 'YES' ? yesPool - yesStart : noPool - noStart
|
||||||
|
|
||||||
|
const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool)
|
||||||
|
|
||||||
|
const saleAmount = (1 - fees) * adjShareValue
|
||||||
|
return saleAmount
|
||||||
|
}
|
|
@ -1,72 +0,0 @@
|
||||||
import { Bet } from '../firebase/bets'
|
|
||||||
import { Contract } from '../firebase/contracts'
|
|
||||||
|
|
||||||
const fees = 0.02
|
|
||||||
|
|
||||||
export function getProbability(pool: { YES: number; NO: number }) {
|
|
||||||
const [yesPool, noPool] = [pool.YES, pool.NO]
|
|
||||||
const numerator = Math.pow(yesPool, 2)
|
|
||||||
const denominator = Math.pow(yesPool, 2) + Math.pow(noPool, 2)
|
|
||||||
return numerator / denominator
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProbabilityAfterBet(
|
|
||||||
pool: { YES: number; NO: number },
|
|
||||||
outcome: 'YES' | 'NO',
|
|
||||||
bet: number
|
|
||||||
) {
|
|
||||||
const [YES, NO] = [
|
|
||||||
pool.YES + (outcome === 'YES' ? bet : 0),
|
|
||||||
pool.NO + (outcome === 'NO' ? bet : 0),
|
|
||||||
]
|
|
||||||
return getProbability({ YES, NO })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDpmWeight(
|
|
||||||
pool: { YES: number; NO: number },
|
|
||||||
bet: number,
|
|
||||||
betChoice: 'YES' | 'NO'
|
|
||||||
) {
|
|
||||||
const [yesPool, noPool] = [pool.YES, pool.NO]
|
|
||||||
|
|
||||||
return betChoice === 'YES'
|
|
||||||
? (bet * Math.pow(noPool, 2)) / (Math.pow(yesPool, 2) + bet * yesPool)
|
|
||||||
: (bet * Math.pow(yesPool, 2)) / (Math.pow(noPool, 2) + bet * noPool)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculatePayout(
|
|
||||||
contract: Contract,
|
|
||||||
bet: Bet,
|
|
||||||
outcome: 'YES' | 'NO' | 'CANCEL'
|
|
||||||
) {
|
|
||||||
const { amount, outcome: betOutcome, dpmWeight } = bet
|
|
||||||
|
|
||||||
if (outcome === 'CANCEL') return amount
|
|
||||||
if (betOutcome !== outcome) return 0
|
|
||||||
|
|
||||||
let { dpmWeights, pool, startPool } = contract
|
|
||||||
|
|
||||||
// Fake data if not set.
|
|
||||||
if (!dpmWeights) dpmWeights = { YES: 100, NO: 100 }
|
|
||||||
|
|
||||||
// Fake data if not set.
|
|
||||||
if (!pool) pool = { YES: 100, NO: 100 }
|
|
||||||
|
|
||||||
const otherOutcome = outcome === 'YES' ? 'NO' : 'YES'
|
|
||||||
const poolSize = pool[otherOutcome] - startPool[otherOutcome]
|
|
||||||
|
|
||||||
return (1 - fees) * (dpmWeight / dpmWeights[outcome]) * poolSize + amount
|
|
||||||
}
|
|
||||||
export function resolvedPayout(contract: Contract, bet: Bet) {
|
|
||||||
if (contract.resolution)
|
|
||||||
return calculatePayout(contract, bet, contract.resolution)
|
|
||||||
throw new Error('Contract was not resolved')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function currentValue(contract: Contract, bet: Bet) {
|
|
||||||
const prob = getProbability(contract.pool)
|
|
||||||
const yesPayout = calculatePayout(contract, bet, 'YES')
|
|
||||||
const noPayout = calculatePayout(contract, bet, 'NO')
|
|
||||||
|
|
||||||
return prob * yesPayout + (1 - prob) * noPayout
|
|
||||||
}
|
|
6
web/lib/firebase/api-call.ts
Normal file
6
web/lib/firebase/api-call.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { getFunctions, httpsCallable } from 'firebase/functions'
|
||||||
|
|
||||||
|
const functions = getFunctions()
|
||||||
|
|
||||||
|
export const cloudFunction = (name: string) => httpsCallable(functions, name)
|
||||||
|
|
|
@ -11,12 +11,21 @@ export type Bet = {
|
||||||
id: string
|
id: string
|
||||||
userId: string
|
userId: string
|
||||||
contractId: string
|
contractId: string
|
||||||
amount: number // Amount of bet
|
|
||||||
outcome: 'YES' | 'NO' // Chosen outcome
|
amount: number // bet size; negative if SELL bet
|
||||||
dpmWeight: number // Dynamic Parimutuel weight
|
outcome: 'YES' | 'NO'
|
||||||
|
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
||||||
|
|
||||||
probBefore: number
|
probBefore: number
|
||||||
probAverage: number
|
|
||||||
probAfter: number
|
probAfter: number
|
||||||
|
|
||||||
|
sale?: {
|
||||||
|
amount: number // amount user makes from sale
|
||||||
|
betId: string // id of bet being sold
|
||||||
|
}
|
||||||
|
|
||||||
|
isSold?: boolean // true if this BUY bet has been sold
|
||||||
|
|
||||||
createdTime: number
|
createdTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export type Contract = {
|
||||||
|
|
||||||
startPool: { YES: number; NO: number }
|
startPool: { YES: number; NO: number }
|
||||||
pool: { YES: number; NO: number }
|
pool: { YES: number; NO: number }
|
||||||
dpmWeights: { YES: number; NO: number }
|
totalShares: { YES: number; NO: number }
|
||||||
|
|
||||||
createdTime: number // Milliseconds since epoch
|
createdTime: number // Milliseconds since epoch
|
||||||
lastUpdatedTime: number // If the question or description was changed
|
lastUpdatedTime: number // If the question or description was changed
|
||||||
|
@ -48,7 +48,7 @@ export function path(contract: Contract) {
|
||||||
|
|
||||||
export function compute(contract: Contract) {
|
export function compute(contract: Contract) {
|
||||||
const { pool, startPool, createdTime, resolutionTime, isResolved } = contract
|
const { pool, startPool, createdTime, resolutionTime, isResolved } = contract
|
||||||
const volume = pool.YES + pool.NO - startPool.YES - startPool.NO
|
const truePool = pool.YES + pool.NO - startPool.YES - startPool.NO
|
||||||
const prob = pool.YES ** 2 / (pool.YES ** 2 + pool.NO ** 2)
|
const prob = pool.YES ** 2 / (pool.YES ** 2 + pool.NO ** 2)
|
||||||
const probPercent = Math.round(prob * 100) + '%'
|
const probPercent = Math.round(prob * 100) + '%'
|
||||||
const startProb =
|
const startProb =
|
||||||
|
@ -57,7 +57,7 @@ export function compute(contract: Contract) {
|
||||||
const resolvedDate = isResolved
|
const resolvedDate = isResolved
|
||||||
? dayjs(resolutionTime).format('MMM D')
|
? dayjs(resolutionTime).format('MMM D')
|
||||||
: undefined
|
: undefined
|
||||||
return { volume, probPercent, startProb, createdDate, resolvedDate }
|
return { truePool, probPercent, startProb, createdDate, resolvedDate }
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = getFirestore(app)
|
const db = getFirestore(app)
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { getFirestore } from '@firebase/firestore'
|
import { getFirestore } from '@firebase/firestore'
|
||||||
import { initializeApp } from 'firebase/app'
|
import { initializeApp } from 'firebase/app'
|
||||||
const firebaseConfig = {
|
|
||||||
|
export const isProd = process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
|
const firebaseConfig = isProd
|
||||||
|
? {
|
||||||
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
||||||
authDomain: 'mantic-markets.firebaseapp.com',
|
authDomain: 'mantic-markets.firebaseapp.com',
|
||||||
projectId: 'mantic-markets',
|
projectId: 'mantic-markets',
|
||||||
|
@ -8,17 +12,17 @@ const firebaseConfig = {
|
||||||
messagingSenderId: '128925704902',
|
messagingSenderId: '128925704902',
|
||||||
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
||||||
measurementId: 'G-SSFK1Q138D',
|
measurementId: 'G-SSFK1Q138D',
|
||||||
}
|
}
|
||||||
|
: {
|
||||||
|
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
||||||
|
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
||||||
|
projectId: 'dev-mantic-markets',
|
||||||
|
storageBucket: 'dev-mantic-markets.appspot.com',
|
||||||
|
messagingSenderId: '134303100058',
|
||||||
|
appId: '1:134303100058:web:27f9ea8b83347251f80323',
|
||||||
|
measurementId: 'G-YJC9E37P37',
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize Firebase
|
// Initialize Firebase
|
||||||
export const app = initializeApp(firebaseConfig)
|
export const app = initializeApp(firebaseConfig)
|
||||||
export const db = getFirestore(app)
|
export const db = getFirestore(app)
|
||||||
|
|
||||||
// try {
|
|
||||||
// // Note: this is still throwing a console error atm...
|
|
||||||
// import('firebase/analytics').then((analytics) => {
|
|
||||||
// analytics.getAnalytics(app)
|
|
||||||
// })
|
|
||||||
// } catch (e) {
|
|
||||||
// console.warn('Analytics were blocked')
|
|
||||||
// }
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import {
|
||||||
Contract,
|
Contract,
|
||||||
getContractFromSlug,
|
getContractFromSlug,
|
||||||
pushNewContract,
|
pushNewContract,
|
||||||
setContract,
|
|
||||||
} from '../firebase/contracts'
|
} from '../firebase/contracts'
|
||||||
import { User } from '../firebase/users'
|
import { User } from '../firebase/users'
|
||||||
import { randomString } from '../util/random-string'
|
import { randomString } from '../util/random-string'
|
||||||
|
@ -38,7 +37,7 @@ export async function createContract(
|
||||||
|
|
||||||
startPool: { YES: startYes, NO: startNo },
|
startPool: { YES: startYes, NO: startNo },
|
||||||
pool: { YES: startYes, NO: startNo },
|
pool: { YES: startYes, NO: startNo },
|
||||||
dpmWeights: { YES: 0, NO: 0 },
|
totalShares: { YES: 0, NO: 0 },
|
||||||
isResolved: false,
|
isResolved: false,
|
||||||
|
|
||||||
// TODO: Set create time to Firestore timestamp
|
// TODO: Set create time to Firestore timestamp
|
||||||
|
@ -49,8 +48,8 @@ export async function createContract(
|
||||||
return await pushNewContract(contract)
|
return await pushNewContract(contract)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calcStartPool(initialProb: number, initialCapital = 200) {
|
export function calcStartPool(initialProbInt: number, initialCapital = 200) {
|
||||||
const p = initialProb / 100.0
|
const p = initialProbInt / 100.0
|
||||||
|
|
||||||
const startYes =
|
const startYes =
|
||||||
p === 0.5
|
p === 0.5
|
||||||
|
|
|
@ -21,10 +21,13 @@ function makeWeights(bids: Bid[]) {
|
||||||
// First pass: calculate all the weights
|
// First pass: calculate all the weights
|
||||||
for (const { yesBid, noBid } of bids) {
|
for (const { yesBid, noBid } of bids) {
|
||||||
const yesWeight =
|
const yesWeight =
|
||||||
(yesBid * Math.pow(noPot, 2)) / (Math.pow(yesPot, 2) + yesBid * yesPot) ||
|
yesBid +
|
||||||
0
|
(yesBid * Math.pow(noPot, 2)) /
|
||||||
|
(Math.pow(yesPot, 2) + yesBid * yesPot) || 0
|
||||||
const noWeight =
|
const noWeight =
|
||||||
(noBid * Math.pow(yesPot, 2)) / (Math.pow(noPot, 2) + noBid * noPot) || 0
|
noBid +
|
||||||
|
(noBid * Math.pow(yesPot, 2)) / (Math.pow(noPot, 2) + noBid * noPot) ||
|
||||||
|
0
|
||||||
|
|
||||||
// Note: Need to calculate weights BEFORE updating pot
|
// Note: Need to calculate weights BEFORE updating pot
|
||||||
yesPot += yesBid
|
yesPot += yesBid
|
||||||
|
@ -53,15 +56,15 @@ export function makeEntries(bids: Bid[]): Entry[] {
|
||||||
const yesWeightsSum = weights.reduce((sum, entry) => sum + entry.yesWeight, 0)
|
const yesWeightsSum = weights.reduce((sum, entry) => sum + entry.yesWeight, 0)
|
||||||
const noWeightsSum = weights.reduce((sum, entry) => sum + entry.noWeight, 0)
|
const noWeightsSum = weights.reduce((sum, entry) => sum + entry.noWeight, 0)
|
||||||
|
|
||||||
|
const potSize = yesPot + noPot - YES_SEED - NO_SEED
|
||||||
|
|
||||||
// Second pass: calculate all the payouts
|
// Second pass: calculate all the payouts
|
||||||
const entries: Entry[] = []
|
const entries: Entry[] = []
|
||||||
|
|
||||||
for (const weight of weights) {
|
for (const weight of weights) {
|
||||||
const { yesBid, noBid, yesWeight, noWeight } = weight
|
const { yesBid, noBid, yesWeight, noWeight } = weight
|
||||||
// Payout: You get your initial bid back, as well as your share of the
|
const yesPayout = (yesWeight / yesWeightsSum) * potSize
|
||||||
// (noPot - seed) according to your yesWeight
|
const noPayout = (noWeight / noWeightsSum) * potSize
|
||||||
const yesPayout = yesBid + (yesWeight / yesWeightsSum) * (noPot - NO_SEED)
|
|
||||||
const noPayout = noBid + (noWeight / noWeightsSum) * (yesPot - YES_SEED)
|
|
||||||
const yesReturn = (yesPayout - yesBid) / yesBid
|
const yesReturn = (yesPayout - yesBid) / yesBid
|
||||||
const noReturn = (noPayout - noBid) / noBid
|
const noReturn = (noPayout - noBid) / noBid
|
||||||
entries.push({ ...weight, yesPayout, noPayout, yesReturn, noReturn })
|
entries.push({ ...weight, yesPayout, noPayout, yesReturn, noReturn })
|
||||||
|
|
|
@ -6,11 +6,11 @@ const formatter = new Intl.NumberFormat('en-US', {
|
||||||
})
|
})
|
||||||
|
|
||||||
export function formatMoney(amount: number) {
|
export function formatMoney(amount: number) {
|
||||||
return 'M$ ' + formatter.format(amount).substring(1)
|
return 'M$ ' + formatter.format(amount).replace('$', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatWithCommas(amount: number) {
|
export function formatWithCommas(amount: number) {
|
||||||
return formatter.format(amount).substring(1)
|
return formatter.format(amount).replace('$', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatPercent(zeroToOne: number) {
|
export function formatPercent(zeroToOne: number) {
|
||||||
|
|
|
@ -97,7 +97,7 @@ function BetsSection(props: { contract: Contract; user: User | null }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Title text="Your bets" />
|
<Title text="Your trades" />
|
||||||
<MyBetsSummary contract={contract} bets={userBets} />
|
<MyBetsSummary contract={contract} bets={userBets} />
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
<ContractBetsTable contract={contract} bets={userBets} />
|
<ContractBetsTable contract={contract} bets={userBets} />
|
||||||
|
|
|
@ -162,8 +162,8 @@ function Contents() {
|
||||||
</p>
|
</p>
|
||||||
<h3 id="how-are-markets-resolved-">How are markets resolved?</h3>
|
<h3 id="how-are-markets-resolved-">How are markets resolved?</h3>
|
||||||
<p>
|
<p>
|
||||||
The creator of the prediction market decides the outcome and earns 0.5%
|
The creator of the prediction market decides the outcome and earns 1% of
|
||||||
of the trade volume for their effort.
|
the betting pool for their effort.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This simple resolution mechanism has surprising benefits in allowing a
|
This simple resolution mechanism has surprising benefits in allowing a
|
||||||
|
|
|
@ -86,7 +86,7 @@ function TableRowEnd(props: { entry: Entry | null; isNew?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<td>{(entry.prob * 100).toFixed(1)}%</td>
|
<td>{(entry.prob * 100).toFixed(1)}%</td>
|
||||||
<td>${(entry.yesBid + entry.yesWeight).toFixed(0)}</td>
|
<td>${entry.yesWeight.toFixed(0)}</td>
|
||||||
{!props.isNew && (
|
{!props.isNew && (
|
||||||
<>
|
<>
|
||||||
<td>${entry.yesPayout.toFixed(0)}</td>
|
<td>${entry.yesPayout.toFixed(0)}</td>
|
||||||
|
@ -99,7 +99,7 @@ function TableRowEnd(props: { entry: Entry | null; isNew?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<td>{(entry.prob * 100).toFixed(1)}%</td>
|
<td>{(entry.prob * 100).toFixed(1)}%</td>
|
||||||
<td>${(entry.noBid + entry.noWeight).toFixed(0)}</td>
|
<td>${entry.noWeight.toFixed(0)}</td>
|
||||||
{!props.isNew && (
|
{!props.isNew && (
|
||||||
<>
|
<>
|
||||||
<td>${entry.noPayout.toFixed(0)}</td>
|
<td>${entry.noPayout.toFixed(0)}</td>
|
||||||
|
@ -149,9 +149,9 @@ function NewBidTable(props: {
|
||||||
|
|
||||||
function randomBid() {
|
function randomBid() {
|
||||||
const bidType = Math.random() < 0.5 ? 'YES' : 'NO'
|
const bidType = Math.random() < 0.5 ? 'YES' : 'NO'
|
||||||
const p = bidType === 'YES' ? nextEntry.prob : 1 - nextEntry.prob
|
// const p = bidType === 'YES' ? nextEntry.prob : 1 - nextEntry.prob
|
||||||
|
|
||||||
const amount = Math.round(p * Math.random() * 300) + 1
|
const amount = Math.floor(Math.random() * 300) + 1
|
||||||
const bid = makeBid(bidType, amount)
|
const bid = makeBid(bidType, amount)
|
||||||
|
|
||||||
bids.splice(steps, 0, bid)
|
bids.splice(steps, 0, bid)
|
||||||
|
@ -238,7 +238,7 @@ function NewBidTable(props: {
|
||||||
// Show a hello world React page
|
// Show a hello world React page
|
||||||
export default function Simulator() {
|
export default function Simulator() {
|
||||||
const [steps, setSteps] = useState(1)
|
const [steps, setSteps] = useState(1)
|
||||||
const [bids, setBids] = useState([{ yesBid: 550, noBid: 450 }])
|
const [bids, setBids] = useState([{ yesBid: 100, noBid: 100 }])
|
||||||
|
|
||||||
const entries = useMemo(
|
const entries = useMemo(
|
||||||
() => makeEntries(bids.slice(0, steps)),
|
() => makeEntries(bids.slice(0, steps)),
|
||||||
|
|
|
@ -4,13 +4,13 @@ import { SEO } from '../components/SEO'
|
||||||
import { Title } from '../components/title'
|
import { Title } from '../components/title'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
|
|
||||||
export default function BetsPage() {
|
export default function TradesPage() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<SEO title="Your bets" description="Your bets" url="/bets" />
|
<SEO title="Your trades" description="Your trades" url="/trades" />
|
||||||
<Title text="Your bets" />
|
<Title text="Your trades" />
|
||||||
{user && <BetsList user={user} />}
|
{user && <BetsList user={user} />}
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
Loading…
Reference in New Issue
Block a user