From b97a65cf2cbe1b1269949b8a0bc98ac4e1a1adda Mon Sep 17 00:00:00 2001 From: mantikoros Date: Mon, 10 Jan 2022 15:07:57 -0600 Subject: [PATCH] refactor data structures, calculations to common directory --- common/bet.ts | 22 +++ common/calculate.ts | 166 ++++++++++++++++++ common/comment.ts | 12 ++ common/contract.ts | 29 +++ common/fees.ts | 4 + common/user.ts | 10 ++ .../src => common}/util/random-string.ts | 2 +- common/util/slugify.ts | 10 ++ functions/src/create-contract.ts | 8 +- functions/src/emails.ts | 4 +- functions/src/place-bet.ts | 6 +- functions/src/resolve-market.ts | 10 +- functions/src/scripts/migrate-contract.ts | 5 +- .../scripts/recalculate-contract-totals.ts | 5 +- .../src/scripts/rename-user-contracts.ts | 3 +- functions/src/sell-bet.ts | 8 +- functions/src/types/bet.ts | 21 --- functions/src/types/contract.ts | 29 --- functions/src/types/user.ts | 10 -- functions/src/update-contract-metrics.ts | 5 +- functions/src/util/slugify.ts | 10 -- functions/src/utils.ts | 4 +- functions/tsconfig.json | 4 +- web/components/bet-panel.tsx | 6 +- web/components/bets-list.tsx | 2 +- web/lib/calculate.ts | 165 ----------------- web/lib/firebase/bets.ts | 26 +-- web/lib/firebase/comments.ts | 18 +- web/lib/firebase/contracts.ts | 37 +--- web/lib/firebase/users.ts | 14 +- web/next.config.js | 1 + web/tsconfig.json | 2 +- 32 files changed, 306 insertions(+), 352 deletions(-) create mode 100644 common/bet.ts create mode 100644 common/calculate.ts create mode 100644 common/comment.ts create mode 100644 common/contract.ts create mode 100644 common/fees.ts create mode 100644 common/user.ts rename {functions/src => common}/util/random-string.ts (84%) create mode 100644 common/util/slugify.ts delete mode 100644 functions/src/types/bet.ts delete mode 100644 functions/src/types/contract.ts delete mode 100644 functions/src/types/user.ts delete mode 100644 functions/src/util/slugify.ts delete mode 100644 web/lib/calculate.ts diff --git a/common/bet.ts b/common/bet.ts new file mode 100644 index 00000000..5356e8f5 --- /dev/null +++ b/common/bet.ts @@ -0,0 +1,22 @@ +export type Bet = { + id: string; + userId: string; + contractId: string; + + amount: number; // bet size; negative if SELL bet + outcome: "YES" | "NO"; + shares: number; // dynamic parimutuel pool weight; negative if SELL bet + + probBefore: number; + probAfter: number; + + sale?: { + amount: number; // amount user makes from sale + betId: string; // id of bet being sold + // TODO: add sale time? + }; + + isSold?: boolean; // true if this BUY bet has been sold + + createdTime: number; +}; diff --git a/common/calculate.ts b/common/calculate.ts new file mode 100644 index 00000000..a5403a56 --- /dev/null +++ b/common/calculate.ts @@ -0,0 +1,166 @@ +import { Bet } from "./bet"; +import { Contract } from "./contract"; +import { FEES } from "./fees"; + +export const blah = () => 999; + +export const 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" | "MKT" +) { + const { amount, outcome: betOutcome, shares } = bet; + + if (outcome === "CANCEL") return amount; + if (outcome === "MKT") return calculateMktPayout(contract, bet); + + if (betOutcome !== outcome) return 0; + + const { totalShares, totalBets } = contract; + + if (totalShares[outcome] === 0) return 0; + + const startPool = contract.startPool.YES + contract.startPool.NO; + const truePool = contract.pool.YES + contract.pool.NO - startPool; + + if (totalBets[outcome] >= truePool) + return (amount / totalBets[outcome]) * truePool; + + const total = totalShares[outcome] - totalBets[outcome]; + const winningsPool = truePool - totalBets[outcome]; + + return (1 - FEES) * (amount + ((shares - amount) / total) * winningsPool); +} + +export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) { + const { amount, outcome, shares } = bet; + const { totalShares, totalBets } = contract; + + const startPool = contract.startPool.YES + contract.startPool.NO; + const truePool = amount + contract.pool.YES + contract.pool.NO - startPool; + + const totalBetsOutcome = totalBets[outcome] + amount; + const totalSharesOutcome = totalShares[outcome] + shares; + + if (totalBetsOutcome >= truePool) + return (amount / totalBetsOutcome) * truePool; + + const total = totalSharesOutcome - totalBetsOutcome; + const winningsPool = truePool - totalBetsOutcome; + + return (1 - FEES) * (amount + ((shares - amount) / total) * winningsPool); +} + +function calculateMktPayout(contract: Contract, bet: Bet) { + const p = + contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2); + const weightedTotal = + p * contract.totalBets.YES + (1 - p) * contract.totalBets.NO; + + const startPool = contract.startPool.YES + contract.startPool.NO; + const truePool = contract.pool.YES + contract.pool.NO - startPool; + + const betP = bet.outcome === "YES" ? p : 1 - p; + + if (weightedTotal >= truePool) { + return ((betP * bet.amount) / weightedTotal) * truePool; + } + + const winningsPool = truePool - weightedTotal; + + const weightedShareTotal = + p * (contract.totalShares.YES - contract.totalBets.YES) + + (1 - p) * (contract.totalShares.NO - contract.totalBets.NO); + + return ( + (1 - FEES) * + (betP * bet.amount + + ((betP * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool) + ); +} + +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; +} + +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; +} diff --git a/common/comment.ts b/common/comment.ts new file mode 100644 index 00000000..42c4c594 --- /dev/null +++ b/common/comment.ts @@ -0,0 +1,12 @@ +// Currently, comments are created after the bet, not atomically with the bet. +// They're uniquely identified by the pair contractId/betId. +export type Comment = { + contractId: string; + betId: string; + text: string; + createdTime: number; + // Denormalized, for rendering comments + userName?: string; + userUsername?: string; + userAvatarUrl?: string; +}; diff --git a/common/contract.ts b/common/contract.ts new file mode 100644 index 00000000..a13363dd --- /dev/null +++ b/common/contract.ts @@ -0,0 +1,29 @@ +export type Contract = { + id: string; + slug: string; // auto-generated; must be unique + + creatorId: string; + creatorName: string; + creatorUsername: string; + + question: string; + description: string; // More info about what the contract is about + outcomeType: "BINARY"; // | 'MULTI' | 'interval' | 'date' + // outcomes: ['YES', 'NO'] + + startPool: { YES: number; NO: number }; + pool: { YES: number; NO: number }; + totalShares: { YES: number; NO: number }; + totalBets: { YES: number; NO: number }; + + createdTime: number; // Milliseconds since epoch + lastUpdatedTime: number; // If the question or description was changed + closeTime?: number; // When no more trading is allowed + + isResolved: boolean; + resolutionTime?: number; // When the contract creator resolved the market + resolution?: "YES" | "NO" | "CANCEL"; // Chosen by creator; must be one of outcomes + + volume24Hours: number; + volume7Days: number; +}; diff --git a/common/fees.ts b/common/fees.ts new file mode 100644 index 00000000..ed477b24 --- /dev/null +++ b/common/fees.ts @@ -0,0 +1,4 @@ +export const PLATFORM_FEE = 0.01; // == 1% +export const CREATOR_FEE = 0.01; + +export const FEES = PLATFORM_FEE + CREATOR_FEE; diff --git a/common/user.ts b/common/user.ts new file mode 100644 index 00000000..5b10ee3b --- /dev/null +++ b/common/user.ts @@ -0,0 +1,10 @@ +export type User = { + id: string; + email: string; + name: string; + username: string; + avatarUrl: string; + balance: number; + createdTime: number; + lastUpdatedTime: number; +}; diff --git a/functions/src/util/random-string.ts b/common/util/random-string.ts similarity index 84% rename from functions/src/util/random-string.ts rename to common/util/random-string.ts index a1cc40a9..128851ab 100644 --- a/functions/src/util/random-string.ts +++ b/common/util/random-string.ts @@ -1 +1 @@ -export const randomString = () => Math.random().toString(16).substr(2, 14) +export const randomString = () => Math.random().toString(16).substr(2, 14); diff --git a/common/util/slugify.ts b/common/util/slugify.ts new file mode 100644 index 00000000..506320b9 --- /dev/null +++ b/common/util/slugify.ts @@ -0,0 +1,10 @@ +export const slugify = (text: any, separator = "-"): string => { + return text + .toString() + .normalize("NFD") // split an accented letter in the base letter and the acent + .replace(/[\u0300-\u036f]/g, "") // remove all previously split accents + .toLowerCase() + .trim() + .replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced) + .replace(/\s+/g, separator); +}; diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index e40a8db9..df129821 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -1,12 +1,12 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { randomString } from './util/random-string' -import { slugify } from './util/slugify' -import { Contract } from './types/contract' import { getUser } from './utils' import { payUser } from '.' -import { User } from './types/user' +import { Contract } from '../../common/contract' +import { User } from '../../common/user' +import { slugify } from '../../common/util/slugify' +import { randomString } from '../../common/util/random-string' export const createContract = functions .runWith({ minInstances: 1 }) diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 0f85638f..60cfab3b 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -1,6 +1,6 @@ import { sendEmail } from './send-email' -import { Contract } from './types/contract' -import { User } from './types/user' +import { Contract } from '../../common/contract' +import { User } from '../../common/user' import { getUser } from './utils' export const sendMarketResolutionEmail = async ( diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 497f5dd1..9d8ec368 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -1,9 +1,9 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { Contract } from './types/contract' -import { User } from './types/user' -import { Bet } from './types/bet' +import { Contract } from '../../common/contract' +import { User } from '../../common/user' +import { Bet } from '../../common/bet' export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index aad9d32c..215b2f73 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -2,15 +2,13 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Contract } from './types/contract' -import { User } from './types/user' -import { Bet } from './types/bet' +import { Contract } from '../../common/contract' +import { User } from '../../common/user' +import { Bet } from '../../common/bet' +import { CREATOR_FEE, PLATFORM_FEE } from '../../common/fees' import { getUser } from './utils' import { sendMarketResolutionEmail } from './emails' -export const PLATFORM_FEE = 0.01 // 1% -export const CREATOR_FEE = 0.01 // 1% - export const resolveMarket = functions .runWith({ minInstances: 1 }) .https.onCall( diff --git a/functions/src/scripts/migrate-contract.ts b/functions/src/scripts/migrate-contract.ts index eb55783e..118c9159 100644 --- a/functions/src/scripts/migrate-contract.ts +++ b/functions/src/scripts/migrate-contract.ts @@ -1,7 +1,8 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Bet } from '../types/bet' -import { Contract } from '../types/contract' + +import { Bet } from '../../../common/bet' +import { Contract } from '../../../common/contract' type DocRef = admin.firestore.DocumentReference diff --git a/functions/src/scripts/recalculate-contract-totals.ts b/functions/src/scripts/recalculate-contract-totals.ts index d691db36..3a368fbb 100644 --- a/functions/src/scripts/recalculate-contract-totals.ts +++ b/functions/src/scripts/recalculate-contract-totals.ts @@ -1,7 +1,8 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Bet } from '../types/bet' -import { Contract } from '../types/contract' + +import { Bet } from '../../../common/bet' +import { Contract } from '../../../common/contract' type DocRef = admin.firestore.DocumentReference diff --git a/functions/src/scripts/rename-user-contracts.ts b/functions/src/scripts/rename-user-contracts.ts index 2debd9e7..8639a590 100644 --- a/functions/src/scripts/rename-user-contracts.ts +++ b/functions/src/scripts/rename-user-contracts.ts @@ -1,6 +1,7 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Contract } from '../types/contract' + +import { Contract } from '../../../common/contract' import { getValues } from '../utils' // Generate your own private key, and set the path below: diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index e68df796..04bb8da7 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 './resolve-market' -import { Bet } from './types/bet' -import { Contract } from './types/contract' -import { User } from './types/user' +import { CREATOR_FEE, PLATFORM_FEE } from '../../common/fees' +import { Contract } from '../../common/contract' +import { User } from '../../common/user' +import { Bet } from '../../common/bet' export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( diff --git a/functions/src/types/bet.ts b/functions/src/types/bet.ts deleted file mode 100644 index 8b540165..00000000 --- a/functions/src/types/bet.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type Bet = { - id: string - userId: string - contractId: string - - amount: number // bet size; negative if SELL bet - outcome: 'YES' | 'NO' - shares: number // dynamic parimutuel pool weight; negative if SELL bet - - probBefore: 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 -} diff --git a/functions/src/types/contract.ts b/functions/src/types/contract.ts deleted file mode 100644 index 7c894ce5..00000000 --- a/functions/src/types/contract.ts +++ /dev/null @@ -1,29 +0,0 @@ -export type Contract = { - id: string - slug: string // auto-generated; must be unique - - creatorId: string - creatorName: string - creatorUsername: string - - question: string - description: string // More info about what the contract is about - outcomeType: 'BINARY' // | 'MULTI' | 'interval' | 'date' - // outcomes: ['YES', 'NO'] - - startPool: { YES: number; NO: number } - pool: { YES: number; NO: number } - totalShares: { YES: number; NO: number } - totalBets: { YES: number; NO: number } - - createdTime: number // Milliseconds since epoch - lastUpdatedTime: number // If the question or description was changed - closeTime?: number // When no more trading is allowed - - isResolved: boolean - resolutionTime?: number // When the contract creator resolved the market - resolution?: 'YES' | 'NO' | 'CANCEL' // Chosen by creator; must be one of outcomes - - volume24Hours: number - volume7Days: number -} diff --git a/functions/src/types/user.ts b/functions/src/types/user.ts deleted file mode 100644 index 8fae1c0b..00000000 --- a/functions/src/types/user.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type User = { - id: string - email: string - name: string - username: string - avatarUrl: string - balance: number - createdTime: number - lastUpdatedTime: number -} \ No newline at end of file diff --git a/functions/src/update-contract-metrics.ts b/functions/src/update-contract-metrics.ts index 8e37b88a..5ebb1d7c 100644 --- a/functions/src/update-contract-metrics.ts +++ b/functions/src/update-contract-metrics.ts @@ -1,9 +1,10 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Contract } from './types/contract' + import { getValues } from './utils' -import { Bet } from './types/bet' +import { Contract } from '../../common/contract' +import { Bet } from '../../common/bet' const firestore = admin.firestore() diff --git a/functions/src/util/slugify.ts b/functions/src/util/slugify.ts deleted file mode 100644 index 82172c3a..00000000 --- a/functions/src/util/slugify.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const slugify = (text: any, separator = '-'): string => { - return text - .toString() - .normalize('NFD') // split an accented letter in the base letter and the acent - .replace(/[\u0300-\u036f]/g, '') // remove all previously split accents - .toLowerCase() - .trim() - .replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced) - .replace(/\s+/g, separator) -} diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 85f5b3fa..a2358243 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -1,7 +1,7 @@ import * as admin from 'firebase-admin' -import { Contract } from './types/contract' -import { User } from './types/user' +import { Contract } from '../../common/contract' +import { User } from '../../common/user' export const getValue = async (collection: string, doc: string) => { const snap = await admin.firestore().collection(collection).doc(doc).get() diff --git a/functions/tsconfig.json b/functions/tsconfig.json index 7ce05d03..6a0ed692 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -9,7 +9,5 @@ "target": "es2017" }, "compileOnSave": true, - "include": [ - "src" - ] + "include": ["src", "../common/**/*.ts"] } diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index a0f0aee2..5ffb27b7 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx' import React, { useEffect, useState } from 'react' import { useUser } from '../hooks/use-user' -import { Contract } from '../lib/firebase/contracts' +import { Contract } from '../../common/contract' import { Col } from './layout/col' import { Row } from './layout/row' import { Spacer } from './layout/spacer' @@ -18,12 +18,12 @@ import { calculateShares, getProbabilityAfterBet, calculatePayoutAfterCorrectBet, -} from '../lib/calculate' +} from '../../common/calculate' import { firebaseLogin } from '../lib/firebase/users' import { AddFundsButton } from './add-funds-button' import { OutcomeLabel } from './outcome-label' import { AdvancedPanel } from './advanced-panel' -import { Bet } from '../lib/firebase/bets' +import { Bet } from '../../common/bet' import { placeBet } from '../lib/firebase/api-call' export function BetPanel(props: { contract: Contract; className?: string }) { diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 4cc376f6..1eae2c53 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -21,7 +21,7 @@ import { calculatePayout, calculateSaleAmount, resolvedPayout, -} from '../lib/calculate' +} from '../../common/calculate' import { sellBet } from '../lib/firebase/api-call' import { ConfirmationButton } from './confirmation-button' import { OutcomeLabel, YesLabel, NoLabel, MarketLabel } from './outcome-label' diff --git a/web/lib/calculate.ts b/web/lib/calculate.ts deleted file mode 100644 index 6e446155..00000000 --- a/web/lib/calculate.ts +++ /dev/null @@ -1,165 +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 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' | 'MKT' -) { - const { amount, outcome: betOutcome, shares } = bet - - if (outcome === 'CANCEL') return amount - if (outcome === 'MKT') return calculateMktPayout(contract, bet) - - if (betOutcome !== outcome) return 0 - - const { totalShares, totalBets } = contract - - if (totalShares[outcome] === 0) return 0 - - const startPool = contract.startPool.YES + contract.startPool.NO - const truePool = contract.pool.YES + contract.pool.NO - startPool - - if (totalBets[outcome] >= truePool) - return (amount / totalBets[outcome]) * truePool - - const total = totalShares[outcome] - totalBets[outcome] - const winningsPool = truePool - totalBets[outcome] - - return (1 - fees) * (amount + ((shares - amount) / total) * winningsPool) -} - -export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) { - const { amount, outcome, shares } = bet - const { totalShares, totalBets } = contract - - const startPool = contract.startPool.YES + contract.startPool.NO - const truePool = amount + contract.pool.YES + contract.pool.NO - startPool - - const totalBetsOutcome = totalBets[outcome] + amount - const totalSharesOutcome = totalShares[outcome] + shares - - if (totalBetsOutcome >= truePool) - return (amount / totalBetsOutcome) * truePool - - const total = totalSharesOutcome - totalBetsOutcome - const winningsPool = truePool - totalBetsOutcome - - return (1 - fees) * (amount + ((shares - amount) / total) * winningsPool) -} - -function calculateMktPayout(contract: Contract, bet: Bet) { - const p = - contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2) - const weightedTotal = - p * contract.totalBets.YES + (1 - p) * contract.totalBets.NO - - const startPool = contract.startPool.YES + contract.startPool.NO - const truePool = contract.pool.YES + contract.pool.NO - startPool - - const betP = bet.outcome === 'YES' ? p : 1 - p - - if (weightedTotal >= truePool) { - return ((betP * bet.amount) / weightedTotal) * truePool - } - - const winningsPool = truePool - weightedTotal - - const weightedShareTotal = - p * (contract.totalShares.YES - contract.totalBets.YES) + - (1 - p) * (contract.totalShares.NO - contract.totalBets.NO) - - return ( - (1 - fees) * - (betP * bet.amount + - ((betP * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool) - ) -} - -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 -} - -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 -} diff --git a/web/lib/firebase/bets.ts b/web/lib/firebase/bets.ts index 9fa67e56..8f6d49eb 100644 --- a/web/lib/firebase/bets.ts +++ b/web/lib/firebase/bets.ts @@ -6,30 +6,10 @@ import { where, } from 'firebase/firestore' import _ from 'lodash' + import { db } from './init' - -export type Bet = { - id: string - userId: string - contractId: string - - amount: number // bet size; negative if SELL bet - outcome: 'YES' | 'NO' - shares: number // dynamic parimutuel pool weight; negative if SELL bet - - probBefore: number - probAfter: number - - sale?: { - amount: number // amount user makes from sale - betId: string // id of bet being sold - // TODO: add sale time? - } - - isSold?: boolean // true if this BUY bet has been sold - - createdTime: number -} +import { Bet } from '../../../common/bet' +export type { Bet } function getBetsCollection(contractId: string) { return collection(db, 'contracts', contractId, 'bets') diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts index d46c6ee9..c55e16eb 100644 --- a/web/lib/firebase/comments.ts +++ b/web/lib/firebase/comments.ts @@ -1,19 +1,9 @@ import { doc, collection, onSnapshot, setDoc } from 'firebase/firestore' -import { db } from './init' -import { User } from './users' -// Currently, comments are created after the bet, not atomically with the bet. -// They're uniquely identified by the pair contractId/betId. -export type Comment = { - contractId: string - betId: string - text: string - createdTime: number - // Denormalized, for rendering comments - userName?: string - userUsername?: string - userAvatarUrl?: string -} +import { db } from './init' +import { User } from '../../../common/user' +import { Comment } from '../../../common/comment' +export type { Comment } export async function createComment( contractId: string, diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 90a0c784..2fd8e786 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -1,4 +1,4 @@ -import { app } from './init' +import dayjs from 'dayjs' import { getFirestore, doc, @@ -14,38 +14,11 @@ import { updateDoc, limit, } from 'firebase/firestore' -import dayjs from 'dayjs' + +import { app } from './init' import { getValues, listenForValues } from './utils' - -export type Contract = { - id: string - slug: string // auto-generated; must be unique - - creatorId: string - creatorName: string - creatorUsername: string - - question: string - description: string // More info about what the contract is about - outcomeType: 'BINARY' // | 'MULTI' | 'interval' | 'date' - // outcomes: ['YES', 'NO'] - - startPool: { YES: number; NO: number } - pool: { YES: number; NO: number } - totalShares: { YES: number; NO: number } - totalBets: { YES: number; NO: number } - - createdTime: number // Milliseconds since epoch - lastUpdatedTime: number // If the question or description was changed - closeTime?: number // When no more trading is allowed - - isResolved: boolean - resolutionTime?: number // When the contract creator resolved the market - resolution?: 'YES' | 'NO' | 'CANCEL' // Chosen by creator; must be one of outcomes - - volume24Hours: number - volume7Days: number -} +import { Contract } from '../../../common/contract' +export type { Contract } export function path(contract: Contract) { // For now, derive username from creatorName diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index f6cd0366..7ee32430 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -19,18 +19,10 @@ import { signInWithPopup, } from 'firebase/auth' -export const STARTING_BALANCE = 1000 +import { User } from '../../../common/user' +export type { User } -export type User = { - id: string - email: string - name: string - username: string - avatarUrl: string - balance: number - createdTime: number - lastUpdatedTime: number -} +export const STARTING_BALANCE = 1000 const db = getFirestore(app) export const auth = getAuth(app) diff --git a/web/next.config.js b/web/next.config.js index a0c27a69..fb6504e4 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -1,6 +1,7 @@ /** @type {import('next').NextConfig} */ module.exports = { reactStrictMode: true, + experimental: { externalDir: true }, images: { domains: ['lh3.googleusercontent.com'], }, diff --git a/web/tsconfig.json b/web/tsconfig.json index 99710e85..dee57196 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -15,6 +15,6 @@ "jsx": "preserve", "incremental": true }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../common/**/*.ts"], "exclude": ["node_modules"] }