From a1aeabeab489541c167d96fbc7df35c67d58f4fc Mon Sep 17 00:00:00 2001 From: mantikoros Date: Mon, 10 Jan 2022 16:49:04 -0600 Subject: [PATCH] move market logic to common --- common/antes.ts | 19 +++++ common/new-bet.ts | 56 ++++++++++++++ common/new-contract.ts | 48 ++++++++++++ common/payouts.ts | 128 +++++++++++++++++++++++++++++++ common/sell-bet.ts | 108 ++++++++++++++++++++++++++ functions/src/create-contract.ts | 76 +----------------- functions/src/place-bet.ts | 55 +------------ functions/src/sell-bet.ts | 106 +------------------------ 8 files changed, 365 insertions(+), 231 deletions(-) create mode 100644 common/antes.ts create mode 100644 common/new-bet.ts create mode 100644 common/new-contract.ts create mode 100644 common/payouts.ts create mode 100644 common/sell-bet.ts diff --git a/common/antes.ts b/common/antes.ts new file mode 100644 index 00000000..ef00e1e9 --- /dev/null +++ b/common/antes.ts @@ -0,0 +1,19 @@ +export const PHANTOM_ANTE = 200; + +export const calcStartPool = (initialProbInt: number, ante?: number) => { + const p = initialProbInt / 100.0; + const totalAnte = PHANTOM_ANTE + (ante || 0); + + const poolYes = + p === 0.5 + ? p * totalAnte + : -(totalAnte * (-p + Math.sqrt((-1 + p) * -p))) / (-1 + 2 * p); + + const poolNo = totalAnte - poolYes; + + const f = PHANTOM_ANTE / totalAnte; + const startYes = f * poolYes; + const startNo = f * poolNo; + + return { startYes, startNo, poolYes, poolNo }; +}; diff --git a/common/new-bet.ts b/common/new-bet.ts new file mode 100644 index 00000000..bf569f4d --- /dev/null +++ b/common/new-bet.ts @@ -0,0 +1,56 @@ +import { Bet } from "./bet"; +import { Contract } from "./contract"; +import { User } from "./user"; + +export const getNewBetInfo = ( + user: User, + outcome: "YES" | "NO", + amount: number, + contract: Contract, + newBetId: string +) => { + const { YES: yesPool, NO: noPool } = contract.pool; + + const newPool = + outcome === "YES" + ? { YES: yesPool + amount, NO: noPool } + : { YES: yesPool, NO: noPool + amount }; + + const shares = + outcome === "YES" + ? amount + (amount * noPool ** 2) / (yesPool ** 2 + amount * yesPool) + : amount + (amount * yesPool ** 2) / (noPool ** 2 + amount * noPool); + + const { YES: yesShares, NO: noShares } = contract.totalShares; + + const newTotalShares = + outcome === "YES" + ? { YES: yesShares + shares, NO: noShares } + : { YES: yesShares, NO: noShares + shares }; + + const { YES: yesBets, NO: noBets } = contract.totalBets; + + const newTotalBets = + outcome === "YES" + ? { YES: yesBets + amount, NO: noBets } + : { YES: yesBets, NO: noBets + amount }; + + const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2); + const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2); + + const newBet: Bet = { + id: newBetId, + userId: user.id, + contractId: contract.id, + amount, + shares, + outcome, + probBefore, + probAfter, + createdTime: Date.now(), + }; + + const newBalance = user.balance - amount; + + return { newBet, newPool, newTotalShares, newTotalBets, newBalance }; +}; diff --git a/common/new-contract.ts b/common/new-contract.ts new file mode 100644 index 00000000..f9718101 --- /dev/null +++ b/common/new-contract.ts @@ -0,0 +1,48 @@ +import { calcStartPool } from "./antes"; +import { Contract } from "./contract"; +import { User } from "./user"; + +export function getNewContract( + id: string, + slug: string, + creator: User, + question: string, + description: string, + initialProb: number, + ante?: number, + closeTime?: number +) { + const { startYes, startNo, poolYes, poolNo } = calcStartPool( + initialProb, + ante + ); + + const contract: Contract = { + id, + slug, + outcomeType: "BINARY", + + creatorId: creator.id, + creatorName: creator.name, + creatorUsername: creator.username, + + question: question.trim(), + description: description.trim(), + + startPool: { YES: startYes, NO: startNo }, + pool: { YES: poolYes, NO: poolNo }, + totalShares: { YES: 0, NO: 0 }, + totalBets: { YES: 0, NO: 0 }, + isResolved: false, + + createdTime: Date.now(), + lastUpdatedTime: Date.now(), + + volume24Hours: 0, + volume7Days: 0, + }; + + if (closeTime) contract.closeTime = closeTime; + + return contract; +} diff --git a/common/payouts.ts b/common/payouts.ts new file mode 100644 index 00000000..349a9c9a --- /dev/null +++ b/common/payouts.ts @@ -0,0 +1,128 @@ +import { Bet } from "./bet"; +import { Contract } from "./contract"; +import { CREATOR_FEE, FEES } from "./fees"; + +export const getCancelPayouts = (truePool: number, bets: Bet[]) => { + console.log("resolved N/A, pool M$", truePool); + + const betSum = sumBy(bets, (b) => b.amount); + + return bets.map((bet) => ({ + userId: bet.userId, + payout: (bet.amount / betSum) * truePool, + })); +}; + +export const getStandardPayouts = ( + outcome: string, + truePool: number, + contract: Contract, + bets: Bet[] +) => { + const [yesBets, noBets] = partition(bets, (bet) => bet.outcome === "YES"); + const winningBets = outcome === "YES" ? yesBets : noBets; + + const betSum = sumBy(winningBets, (b) => b.amount); + + if (betSum >= truePool) return getCancelPayouts(truePool, winningBets); + + const creatorPayout = CREATOR_FEE * truePool; + console.log( + "resolved", + outcome, + "pool: M$", + truePool, + "creator fee: M$", + creatorPayout + ); + + const shareDifferenceSum = sumBy(winningBets, (b) => b.shares - b.amount); + + const winningsPool = truePool - betSum; + + const winnerPayouts = winningBets.map((bet) => ({ + userId: bet.userId, + payout: + (1 - FEES) * + (bet.amount + + ((bet.shares - bet.amount) / shareDifferenceSum) * winningsPool), + })); + + return winnerPayouts.concat([ + { userId: contract.creatorId, payout: creatorPayout }, + ]); // add creator fee +}; + +export const getMktPayouts = ( + truePool: number, + contract: Contract, + bets: Bet[] +) => { + const p = + contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2); + console.log("Resolved MKT at p=", p, "pool: $M", truePool); + + const [yesBets, noBets] = partition(bets, (bet) => bet.outcome === "YES"); + + const weightedBetTotal = + p * sumBy(yesBets, (b) => b.amount) + + (1 - p) * sumBy(noBets, (b) => b.amount); + + if (weightedBetTotal >= truePool) { + return bets.map((bet) => ({ + userId: bet.userId, + payout: + (((bet.outcome === "YES" ? p : 1 - p) * bet.amount) / + weightedBetTotal) * + truePool, + })); + } + + const winningsPool = truePool - weightedBetTotal; + + const weightedShareTotal = + p * sumBy(yesBets, (b) => b.shares - b.amount) + + (1 - p) * sumBy(noBets, (b) => b.shares - b.amount); + + const yesPayouts = yesBets.map((bet) => ({ + userId: bet.userId, + payout: + (1 - FEES) * + (p * bet.amount + + ((p * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool), + })); + + const noPayouts = noBets.map((bet) => ({ + userId: bet.userId, + payout: + (1 - FEES) * + ((1 - p) * bet.amount + + (((1 - p) * (bet.shares - bet.amount)) / weightedShareTotal) * + winningsPool), + })); + + const creatorPayout = CREATOR_FEE * truePool; + + return [ + ...yesPayouts, + ...noPayouts, + { userId: contract.creatorId, payout: creatorPayout }, + ]; +}; + +const partition = (array: T[], f: (t: T) => boolean) => { + const yes = []; + const no = []; + + for (let t of array) { + if (f(t)) yes.push(t); + else no.push(t); + } + + return [yes, no] as [T[], T[]]; +}; + +const sumBy = (array: T[], f: (t: T) => number) => { + const values = array.map(f); + return values.reduce((prev, cur) => prev + cur, 0); +}; diff --git a/common/sell-bet.ts b/common/sell-bet.ts new file mode 100644 index 00000000..72d5cf4f --- /dev/null +++ b/common/sell-bet.ts @@ -0,0 +1,108 @@ +import { Bet } from "./bet"; +import { Contract } from "./contract"; +import { CREATOR_FEE, PLATFORM_FEE } from "./fees"; +import { User } from "./user"; + +export 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 { YES: yesBets, NO: noBets } = contract.totalBets; + + 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 newTotalBets = + outcome === "YES" + ? { YES: yesBets - amount, NO: noBets } + : { YES: yesBets, NO: noBets - amount }; + + 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, + newTotalBets, + newBalance, + creatorFee, + }; +}; diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index df129821..dbe3806b 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -1,12 +1,11 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { getUser } from './utils' -import { payUser } from '.' +import { chargeUser, getUser } from './utils' import { Contract } from '../../common/contract' -import { User } from '../../common/user' import { slugify } from '../../common/util/slugify' import { randomString } from '../../common/util/random-string' +import { getNewContract } from '../../common/new-contract' export const createContract = functions .runWith({ minInstances: 1 }) @@ -62,7 +61,7 @@ export const createContract = functions closeTime ) - if (ante) await payUser([creator.id, -ante]) + if (ante) await chargeUser(creator.id, ante) await contractRef.create(contract) return { status: 'success', contract } @@ -70,7 +69,7 @@ export const createContract = functions ) const getSlug = async (question: string) => { - const proposedSlug = slugify(question).substring(0, 35) + const proposedSlug = slugify(question) const preexistingContract = await getContractFromSlug(proposedSlug) @@ -79,73 +78,6 @@ const getSlug = async (question: string) => { : proposedSlug } -function getNewContract( - id: string, - slug: string, - creator: User, - question: string, - description: string, - initialProb: number, - ante?: number, - closeTime?: number -) { - const { startYes, startNo, poolYes, poolNo } = calcStartPool( - initialProb, - ante - ) - - const contract: Contract = { - id, - slug, - outcomeType: 'BINARY', - - creatorId: creator.id, - creatorName: creator.name, - creatorUsername: creator.username, - - question: question.trim(), - description: description.trim(), - - startPool: { YES: startYes, NO: startNo }, - pool: { YES: poolYes, NO: poolNo }, - totalShares: { YES: 0, NO: 0 }, - totalBets: { YES: 0, NO: 0 }, - isResolved: false, - - createdTime: Date.now(), - lastUpdatedTime: Date.now(), - - volume24Hours: 0, - volume7Days: 0, - } - - if (closeTime) contract.closeTime = closeTime - - return contract -} - -const calcStartPool = ( - initialProbInt: number, - ante?: number, - phantomAnte = 200 -) => { - const p = initialProbInt / 100.0 - const totalAnte = phantomAnte + (ante || 0) - - const poolYes = - p === 0.5 - ? p * totalAnte - : -(totalAnte * (-p + Math.sqrt((-1 + p) * -p))) / (-1 + 2 * p) - - const poolNo = totalAnte - poolYes - - const f = phantomAnte / totalAnte - const startYes = f * poolYes - const startNo = f * poolNo - - return { startYes, startNo, poolYes, poolNo } -} - const firestore = admin.firestore() export async function getContractFromSlug(slug: string) { diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 9d8ec368..dee470d6 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -3,7 +3,7 @@ import * as admin from 'firebase-admin' import { Contract } from '../../common/contract' import { User } from '../../common/user' -import { Bet } from '../../common/bet' +import { getNewBetInfo } from '../../common/new-bet' export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( @@ -67,56 +67,3 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( ) const firestore = admin.firestore() - -const getNewBetInfo = ( - user: User, - outcome: 'YES' | 'NO', - amount: number, - contract: Contract, - newBetId: string -) => { - const { YES: yesPool, NO: noPool } = contract.pool - - const newPool = - outcome === 'YES' - ? { YES: yesPool + amount, NO: noPool } - : { YES: yesPool, NO: noPool + amount } - - const shares = - outcome === 'YES' - ? amount + (amount * noPool ** 2) / (yesPool ** 2 + amount * yesPool) - : amount + (amount * yesPool ** 2) / (noPool ** 2 + amount * noPool) - - const { YES: yesShares, NO: noShares } = contract.totalShares - - const newTotalShares = - outcome === 'YES' - ? { YES: yesShares + shares, NO: noShares } - : { YES: yesShares, NO: noShares + shares } - - const { YES: yesBets, NO: noBets } = contract.totalBets - - const newTotalBets = - outcome === 'YES' - ? { YES: yesBets + amount, NO: noBets } - : { YES: yesBets, NO: noBets + amount } - - const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2) - const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2) - - const newBet: Bet = { - id: newBetId, - userId: user.id, - contractId: contract.id, - amount, - shares, - outcome, - probBefore, - probAfter, - createdTime: Date.now(), - } - - const newBalance = user.balance - amount - - return { newBet, newPool, newTotalShares, newTotalBets, newBalance } -} diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index 04bb8da7..840435f1 100644 --- a/functions/src/sell-bet.ts +++ b/functions/src/sell-bet.ts @@ -1,10 +1,10 @@ import * as admin from 'firebase-admin' import * as functions from 'firebase-functions' -import { CREATOR_FEE, PLATFORM_FEE } from '../../common/fees' import { Contract } from '../../common/contract' import { User } from '../../common/user' import { Bet } from '../../common/bet' +import { getSellBetInfo } from '../../common/sell-bet' export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( @@ -80,107 +80,3 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( ) 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 { YES: yesBets, NO: noBets } = contract.totalBets - - 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 newTotalBets = - outcome === 'YES' - ? { YES: yesBets - amount, NO: noBets } - : { YES: yesBets, NO: noBets - amount } - - 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, - newTotalBets, - newBalance, - creatorFee, - } -}