Merge branch 'main' into swap3

This commit is contained in:
Austin Chen 2022-06-25 16:48:00 -05:00
commit 2cf672fac1
239 changed files with 9002 additions and 5090 deletions

View File

@ -48,8 +48,8 @@ jobs:
- name: Run Typescript checker on web client
if: ${{ success() || failure() }}
working-directory: web
run: tsc --pretty --project tsconfig.json --noEmit
run: tsc -b -v --pretty
- name: Run Typescript checker on cloud functions
if: ${{ success() || failure() }}
working-directory: functions
run: tsc --pretty --project tsconfig.json --noEmit
run: tsc -b -v --pretty

2
.gitignore vendored
View File

@ -3,3 +3,5 @@
.vercel
node_modules
yarn-error.log
firebase-debug.log

14
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"toba.vsfire",
"bradlc.vscode-tailwindcss"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}

View File

@ -1,7 +1,7 @@
{
"javascript.preferences.importModuleSpecifier": "shortest",
"typescript.preferences.importModuleSpecifier": "shortest",
"files.eol": "\r\n",
"files.eol": "\n",
"search.exclude": {
"**/node_modules": true,
"**/package-lock.json": true,

View File

@ -17,6 +17,14 @@ module.exports = {
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
],

5
common/.gitignore vendored
View File

@ -1,6 +1,5 @@
# Compiled JavaScript files
lib/**/*.js
lib/**/*.js.map
lib/
# TypeScript v1 declaration files
typings/
@ -10,4 +9,4 @@ node_modules/
package-lock.json
ui-debug.log
firebase-debug.log
firebase-debug.log

View File

@ -1,4 +1,4 @@
import { sum, groupBy, mapValues, sumBy } from 'lodash'
import { sum, groupBy, mapValues, sumBy, partition } from 'lodash'
import { CPMMContract } from './contract'
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, noFees, PLATFORM_FEE } from './fees'
@ -260,27 +260,30 @@ export function addCpmmLiquidity(
return { newPool, liquidity, newP }
}
const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => {
const oldLiquidity = getCpmmLiquidity(l.pool, p)
const newPool = addObjects(l.pool, { YES: l.amount, NO: l.amount })
const newLiquidity = getCpmmLiquidity(newPool, p)
const liquidity = newLiquidity - oldLiquidity
return liquidity
}
export function getCpmmLiquidityPoolWeights(
contract: CPMMContract,
liquidities: LiquidityProvision[]
) {
const { p } = contract
const [antes, nonAntes] = partition(liquidities, (l) => !!l.isAnte)
const liquidityShares = liquidities.map((l) => {
const oldLiquidity = getCpmmLiquidity(l.pool, p)
const calcLiqudity = calculateLiquidityDelta(contract.p)
const liquidityShares = nonAntes.map(calcLiqudity)
const newPool = addObjects(l.pool, { YES: l.amount, NO: l.amount })
const newLiquidity = getCpmmLiquidity(newPool, p)
const liquidity = newLiquidity - oldLiquidity
return liquidity
})
const shareSum = sum(liquidityShares)
const shareSum = sum(liquidityShares) + sum(antes.map(calcLiqudity))
const weights = liquidityShares.map((s, i) => ({
weight: s / shareSum,
providerId: liquidities[i].userId,
providerId: nonAntes[i].userId,
}))
const userWeights = groupBy(weights, (w) => w.providerId)
@ -290,22 +293,13 @@ export function getCpmmLiquidityPoolWeights(
return totalUserWeights
}
// export function removeCpmmLiquidity(
// contract: CPMMContract,
// liquidity: number
// ) {
// const { YES, NO } = contract.pool
// const poolLiquidity = getCpmmLiquidity({ YES, NO })
// const p = getCpmmProbability({ YES, NO }, contract.p)
export function getUserLiquidityShares(
userId: string,
contract: CPMMContract,
liquidities: LiquidityProvision[]
) {
const weights = getCpmmLiquidityPoolWeights(contract, liquidities)
const userWeight = weights[userId] ?? 0
// const f = liquidity / poolLiquidity
// const [payoutYes, payoutNo] = [f * YES, f * NO]
// const betAmount = Math.abs(payoutYes - payoutNo)
// const betOutcome = p >= 0.5 ? 'NO' : 'YES' // opposite side as adding liquidity
// const payout = Math.min(payoutYes, payoutNo)
// const newPool = { YES: YES - payoutYes, NO: NO - payoutNo }
// return { newPool, payout, betAmount, betOutcome }
// }
return mapValues(contract.pool, (shares) => userWeight * shares)
}

View File

@ -346,10 +346,6 @@ function calculateMktDpmPayout(contract: DPMContract, bet: Bet) {
probs = mapValues(totalShares, (shares) => shares ** 2 / squareSum)
}
const weightedShareTotal = sumBy(Object.keys(totalShares), (outcome) => {
return probs[outcome] * totalShares[outcome]
})
const { outcome, amount, shares } = bet
const poolFrac =
@ -359,11 +355,11 @@ function calculateMktDpmPayout(contract: DPMContract, bet: Bet) {
(outcome) => {
return (
(probs[outcome] * (bet as NumericBet).allOutcomeShares[outcome]) /
weightedShareTotal
totalShares[outcome]
)
}
)
: (probs[outcome] * shares) / weightedShareTotal
: (probs[outcome] * shares) / totalShares[outcome]
const totalPool = sum(Object.values(pool))
const winnings = poolFrac * totalPool

View File

@ -142,6 +142,10 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
const profit = payout + saleValue + redeemed - totalInvested
const profitPercent = (profit / totalInvested) * 100
const hasShares = Object.values(totalShares).some(
(shares) => shares > 0
)
return {
invested: Math.max(0, currentInvested),
payout,
@ -149,6 +153,7 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
profit,
profitPercent,
totalShares,
hasShares,
}
}
@ -160,6 +165,7 @@ export function getContractBetNullMetrics() {
profit: 0,
profitPercent: 0,
totalShares: {} as { [outcome: string]: number },
hasShares: false,
}
}

View File

@ -1,3 +1,5 @@
import { difference } from 'lodash'
export const CATEGORIES = {
politics: 'Politics',
technology: 'Technology',
@ -12,10 +14,19 @@ export const CATEGORIES = {
crypto: 'Crypto',
gaming: 'Gaming',
fun: 'Fun',
} as { [category: string]: string }
}
export type category = keyof typeof CATEGORIES
export const TO_CATEGORY = Object.fromEntries(
Object.entries(CATEGORIES).map(([k, v]) => [v, k])
)
export const CATEGORY_LIST = Object.keys(CATEGORIES)
export const EXCLUDED_CATEGORIES: category[] = ['fun', 'manifold', 'personal']
export const DEFAULT_CATEGORIES = difference(
CATEGORY_LIST,
EXCLUDED_CATEGORIES
)

View File

@ -58,6 +58,19 @@ export const charities: Charity[] = [
- Promoting long-term thinking`,
tags: ['Featured'] as CharityTag[],
},
{
name: 'New Science',
website: 'https://newscience.org/',
photo: 'https://i.imgur.com/C7PoR4q.png',
preview:
'Facilitating scientific breakthroughs by empowering the next generation of scientists and building the 21st century institutions of basic science.',
description: `As its first major project, in the summer of 2022, New Science will run an in-person research fellowship in Boston for young life scientists, during which they will independently explore an ambitious high-risk scientific idea they couldnt work on otherwise and start building the foundations for a bigger research project, while having much more freedom than they could expect in their normal research environment but also much more support from us. This is inspired by Cold Spring Harbor Laboratory, which started as a place where leading molecular biologists came for the summer to hang out and work on random projects together, and which eventually housed 8 Nobel Prize winners.
As its second major project, in the fall of 2022, New Science will run an in-person 12-month-long fellowship for young scientists starting to directly attack the biggest structural issues of the established institutions of science. We will double down on things that worked well during the summer fellowship, while extending the fellowship to one year, thus allowing researchers to make much more progress and will strive to provide them as much scientific leverage as possible.
In several years, New Science will start funding entire labs outside of academia and then will be creating an entire network of scientific organizations, while supporting the broader scientific ecosystem that will constitute the 21st century institutions of basic science.`,
tags: ['Featured'] as CharityTag[],
},
{
name: 'Global Health and Development Fund',
website: 'https://funds.effectivealtruism.org/funds/global-development',
@ -472,9 +485,9 @@ Future plans: We expect to focus on similar theoretical problems in alignment un
name: 'The Trevor Project',
website: 'https://www.thetrevorproject.org/',
photo: 'https://i.imgur.com/QN4mVNn.jpeg',
preview: 'The Trevor Project is the worlds largest suicide prevention and crisis intervention organization for LGBTQ (lesbian, gay, bisexual, transgender, queer, and questioning) young people.',
description:
`Two decades ago, we responded to a health crisis. Now were building a safer, more-inclusive world. LGBTQ young people are four times more likely to attempt suicide, and suicide remains the second leading cause of death among all young people in the U.S.
preview:
'The Trevor Project is the worlds largest suicide prevention and crisis intervention organization for LGBTQ (lesbian, gay, bisexual, transgender, queer, and questioning) young people.',
description: `Two decades ago, we responded to a health crisis. Now were building a safer, more-inclusive world. LGBTQ young people are four times more likely to attempt suicide, and suicide remains the second leading cause of death among all young people in the U.S.
Our Mission
To end suicide among lesbian, gay, bisexual, transgender, queer & questioning young people.
@ -485,6 +498,24 @@ Future plans: We expect to focus on similar theoretical problems in alignment un
Our Goal
To serve 1.8 million crisis contacts annually, by the end of our 25th year, while continuing to innovate on our core services.`,
},
{
name: 'ACLU',
website: 'https://www.aclu.org/',
photo: 'https://i.imgur.com/nbSYuDC.png',
preview:
'The ACLU works in the courts, legislatures, and communities to defend and preserve the individual rights and liberties guaranteed to all people in this country by the Constitution and laws of the United States.',
description: `
THREE THINGS TO KNOW ABOUT THE ACLU
We protect American values. In many ways, the ACLU is the nation's most conservative organization. Our job is to conserve America's original civic values - the Constitution and the Bill of Rights - and defend the rights of every man, woman and child in this country.
We're not anti-anything. The only things we fight are attempts to take away or limit your civil liberties, like your right to practice any religion you want (or none at all); or to decide in private whether or not to have a child; or to speak out - for or against - anything at all; or to be treated with equality and fairness, no matter who you are.
We're there for you. Rich or poor, straight or gay, black or white or brown, urban or rural, pious or atheist, American-born or foreign-born, able-bodied or living with a disability. Every person in this country should have the same basic rights. And since our founding in 1920, we've been working hard to make sure no one takes them away.
The American Civil Liberties Union is our nation's guardian of liberty, working daily in courts, legislatures and communities to defend and preserve the individual rights and liberties that the Constitution and laws of the United States guarantee everyone in this country.
"So long as we have enough people in this country willing to fight for their rights, we'll be called a democracy," ACLU Founder Roger Baldwin said.
The U.S. Constitution and the Bill of Rights trumpet our aspirations for the kind of society that we want to be. But for much of our history, our nation failed to fulfill the promise of liberty for whole groups of people.`,
},
].map((charity) => {
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
return {

View File

@ -2,7 +2,8 @@
// They're uniquely identified by the pair contractId/betId.
export type Comment = {
id: string
contractId: string
contractId?: string
groupId?: string
betId?: string
answerOutcome?: string
replyToCommentId?: string

View File

@ -33,6 +33,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
isResolved: boolean
resolutionTime?: number // When the contract creator resolved the market
resolution?: string
resolutionProbability?: number,
closeEmailsSent?: number
@ -94,6 +95,9 @@ export type outcomeType = AnyOutcomeType['outcomeType']
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'NUMERIC'] as const
export const MAX_QUESTION_LENGTH = 480
export const MAX_DESCRIPTION_LENGTH = 10000
export const MAX_TAG_LENGTH = 60
export const CPMM_MIN_POOL_QTY = 0.01

View File

@ -12,8 +12,7 @@ export const DEV_CONFIG: EnvConfig = {
appId: '1:134303100058:web:27f9ea8b83347251f80323',
measurementId: 'G-YJC9E37P37',
},
functionEndpoints: {
placebet: 'https://placebet-w3txbmd3ba-uc.a.run.app',
createmarket: 'https://createmarket-w3txbmd3ba-uc.a.run.app',
},
cloudRunId: 'w3txbmd3ba',
cloudRunRegion: 'uc',
amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3',
}

View File

@ -1,9 +1,12 @@
export type V2CloudFunction = 'placebet' | 'createmarket'
export type EnvConfig = {
domain: string
firebaseConfig: FirebaseConfig
functionEndpoints: Record<V2CloudFunction, string>
amplitudeApiKey?: string
// IDs for v2 cloud functions -- find these by deploying a cloud function and
// examining the URL, https://[name]-[cloudRunId]-[cloudRunRegion].a.run.app
cloudRunId: string
cloudRunRegion: string
// Access controls
adminEmails: string[]
@ -30,6 +33,8 @@ type FirebaseConfig = {
export const PROD_CONFIG: EnvConfig = {
domain: 'manifold.markets',
amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15',
firebaseConfig: {
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
authDomain: 'mantic-markets.firebaseapp.com',
@ -40,10 +45,8 @@ export const PROD_CONFIG: EnvConfig = {
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
measurementId: 'G-SSFK1Q138D',
},
functionEndpoints: {
placebet: 'https://placebet-nggbo3neva-uc.a.run.app',
createmarket: 'https://createmarket-nggbo3neva-uc.a.run.app',
},
cloudRunId: 'nggbo3neva',
cloudRunRegion: 'uc',
adminEmails: [
'akrolsmir@gmail.com', // Austin
'jahooma@gmail.com', // James

View File

@ -12,11 +12,8 @@ export const THEOREMONE_CONFIG: EnvConfig = {
appId: '1:698012149198:web:b342af75662831aa84b79f',
measurementId: 'G-Y3EZ1WNT6E',
},
// TODO: fill in real endpoints for T1
functionEndpoints: {
placebet: 'https://placebet-nggbo3neva-uc.a.run.app',
createmarket: 'https://createmarket-nggbo3neva-uc.a.run.app',
},
cloudRunId: 'nggbo3neva', // TODO: fill in real ID for T1
cloudRunRegion: 'uc',
adminEmails: [...PROD_CONFIG.adminEmails, 'david.glidden@theoremone.co'],
whitelistEmail: '@theoremone.co',
moneyMoniker: 'T$',

View File

@ -1,6 +1,6 @@
export const PLATFORM_FEE = 0.01
export const CREATOR_FEE = 0.06
export const LIQUIDITY_FEE = 0.06
export const PLATFORM_FEE = 0
export const CREATOR_FEE = 0.1
export const LIQUIDITY_FEE = 0
export const DPM_PLATFORM_FEE = 0.01
export const DPM_CREATOR_FEE = 0.04

View File

@ -1,23 +0,0 @@
export type Fold = {
id: string
slug: string
name: string
about: string
curatorId: string // User id
createdTime: number
tags: string[]
lowercaseTags: string[]
contractIds: string[]
excludedContractIds: string[]
// Invariant: exactly one of the following is defined.
// Default: creatorIds: undefined, excludedCreatorIds: []
creatorIds?: string[]
excludedCreatorIds?: string[]
followCount: number
disallowMarketCreation?: boolean
}

4
common/follow.ts Normal file
View File

@ -0,0 +1,4 @@
export type Follow = {
userId: string
timestamp: number
}

15
common/group.ts Normal file
View File

@ -0,0 +1,15 @@
export type Group = {
id: string
slug: string
name: string
about: string
creatorId: string // User id
createdTime: number
mostRecentActivityTime: number
memberIds: string[] // User ids
anyoneCanJoin: boolean
contractIds: string[]
}
export const MAX_GROUP_NAME_LENGTH = 75
export const MAX_ABOUT_LENGTH = 140
export const MAX_ID_LENGTH = 60

35
common/manalink.ts Normal file
View File

@ -0,0 +1,35 @@
export type Manalink = {
// The link to send: https://manifold.markets/send/{slug}
// Also functions as the unique id for the link.
slug: string
// Note: we assume both fromId and toId are of SourceType 'USER'
fromId: string
// Displayed to people claiming the link
message: string
// How much to send with the link
amount: number
token: 'M$' // TODO: could send eg YES shares too??
createdTime: number
// If null, the link is valid forever
expiresTime: number | null
// If null, the link can be used infinitely
maxUses: number | null
// Used for simpler caching
claimedUserIds: string[]
// Successful redemptions of the link
claims: Claim[]
}
export type Claim = {
toId: string
// The ID of the successful txn that tracks the money moved
txnId: string
claimedTime: number
}

View File

@ -14,6 +14,14 @@ export type Notification = {
sourceUserName?: string
sourceUserUsername?: string
sourceUserAvatarUrl?: string
sourceText?: string
sourceContractTitle?: string
sourceContractCreatorUsername?: string
sourceContractSlug?: string
sourceSlug?: string
sourceTitle?: string
}
export type notification_source_types =
| 'contract'
@ -24,6 +32,7 @@ export type notification_source_types =
| 'follow'
| 'tip'
| 'admin_message'
| 'group'
export type notification_source_update_types =
| 'created'
@ -42,3 +51,5 @@ export type notification_reason_types =
| 'reply_to_users_answer'
| 'reply_to_users_comment'
| 'on_new_follow'
| 'you_follow_user'
| 'added_you_to_group'

View File

@ -0,0 +1,27 @@
import { groupBy, mapValues, sum, sumBy } from 'lodash'
import { Txn } from './txn'
// Returns a map of charity ids to the amount of M$ matched
export function quadraticMatches(
allCharityTxns: Txn[],
matchingPool: number
): Record<string, number> {
// For each charity, group the donations by each individual donor
const donationsByCharity = groupBy(allCharityTxns, 'toId')
const donationsByDonors = mapValues(donationsByCharity, (txns) =>
groupBy(txns, 'fromId')
)
// Weight for each charity = [sum of sqrt(individual donor)] ^ 2
const weights = mapValues(donationsByDonors, (byDonor) => {
const sumByDonor = Object.values(byDonor).map((txns) =>
sumBy(txns, 'amount')
)
const sumOfRoots = sumBy(sumByDonor, Math.sqrt)
return sumOfRoots ** 2
})
// Then distribute the matching pool based on the individual weights
const totalWeight = sum(Object.values(weights))
return mapValues(weights, (weight) => matchingPool * (weight / totalWeight))
}

View File

@ -7,7 +7,12 @@ import { getPayouts } from './payouts'
export function scoreCreators(contracts: Contract[]) {
const creatorScore = mapValues(
groupBy(contracts, ({ creatorId }) => creatorId),
(contracts) => sumBy(contracts, ({ pool }) => pool.YES + pool.NO)
(contracts) =>
sumBy(
contracts.map((contract) => {
return contract.volume
})
)
)
return creatorScore

View File

@ -1,22 +1,18 @@
import { Bet } from './bet'
import {
getDpmProbability,
calculateDpmShareValue,
deductDpmFees,
getDpmOutcomeProbability,
} from './calculate-dpm'
import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm'
import { CPMMContract, DPMContract } from './contract'
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
import { User } from './user'
export const getSellBetInfo = (
user: User,
bet: Bet,
contract: DPMContract,
newBetId: string
) => {
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
const { pool, totalShares, totalBets } = contract
const { id: betId, amount, shares, outcome, loanAmount } = bet
const { id: betId, amount, shares, outcome } = bet
const adjShareValue = calculateDpmShareValue(contract, bet)
@ -29,8 +25,8 @@ export const getSellBetInfo = (
const newTotalBets = { ...totalBets, [outcome]: totalBets[outcome] - amount }
const probBefore = getDpmProbability(totalShares)
const probAfter = getDpmProbability(newTotalShares)
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
const profit = adjShareValue - amount
@ -54,9 +50,7 @@ export const getSellBetInfo = (
creatorFee
)
const newBet: Bet = {
id: newBetId,
userId: user.id,
const newBet: CandidateBet<Bet> = {
contractId: contract.id,
amount: -adjShareValue,
shares: -shares,
@ -71,25 +65,20 @@ export const getSellBetInfo = (
fees,
}
const newBalance = user.balance + saleAmount - (loanAmount ?? 0)
return {
newBet,
newPool,
newTotalShares,
newTotalBets,
newBalance,
fees,
}
}
export const getCpmmSellBetInfo = (
user: User,
shares: number,
outcome: 'YES' | 'NO',
contract: CPMMContract,
prevLoanAmount: number,
newBetId: string
prevLoanAmount: number
) => {
const { pool, p } = contract
@ -100,8 +89,6 @@ export const getCpmmSellBetInfo = (
)
const loanPaid = Math.min(prevLoanAmount, saleValue)
const netAmount = saleValue - loanPaid
const probBefore = getCpmmProbability(pool, p)
const probAfter = getCpmmProbability(newPool, p)
@ -115,9 +102,7 @@ export const getCpmmSellBetInfo = (
fees.creatorFee
)
const newBet: Bet = {
id: newBetId,
userId: user.id,
const newBet: CandidateBet<Bet> = {
contractId: contract.id,
amount: -saleValue,
shares: -shares,
@ -129,13 +114,10 @@ export const getCpmmSellBetInfo = (
fees,
}
const newBalance = user.balance + netAmount
return {
newBet,
newPool,
newP,
newBalance,
fees,
}
}

View File

@ -1,6 +1,8 @@
{
"compilerOptions": {
"baseUrl": "../",
"composite": true,
"module": "commonjs",
"moduleResolution": "node",
"noImplicitReturns": true,
"outDir": "lib",

View File

@ -1,6 +1,9 @@
// A txn (pronounced "texan") respresents a payment between two ids on Manifold
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
export type Txn = {
type AnyTxnType = Donation | Tip | Manalink
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
export type Txn<T extends AnyTxnType = AnyTxnType> = {
id: string
createdTime: number
@ -13,9 +16,36 @@ export type Txn = {
amount: number
token: 'M$' // | 'USD' | MarketOutcome
category: 'CHARITY' // | 'BET' | 'TIP'
category: 'CHARITY' | 'MANALINK' | 'TIP' // | 'BET'
// Any extra data
data?: { [key: string]: any }
// Human-readable description
description?: string
} & T
type Donation = {
fromType: 'USER'
toType: 'CHARITY'
category: 'CHARITY'
}
export type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
type Tip = {
fromType: 'USER'
toType: 'USER'
category: 'TIP'
data: {
contractId: string
commentId: string
}
}
type Manalink = {
fromType: 'USER'
toType: 'USER'
category: 'MANALINK'
}
export type DonationTxn = Txn & Donation
export type TipTxn = Txn & Tip
export type ManalinkTxn = Txn & Manalink

View File

@ -15,8 +15,22 @@ export type User = {
balance: number
totalDeposits: number
totalPnLCached: number
creatorVolumeCached: number
profitCached: {
daily: number
weekly: number
monthly: number
allTime: number
}
creatorVolumeCached: {
daily: number
weekly: number
monthly: number
allTime: number
}
followerCountCached: number
followedCategories?: string[]
}
@ -40,3 +54,11 @@ export type PrivateUser = {
}
export type notification_subscribe_types = 'all' | 'less' | 'none'
export type PortfolioMetrics = {
investmentValue: number
balance: number
totalDeposits: number
timestamp: number
userId: string
}

View File

@ -7,6 +7,6 @@ export const cleanUsername = (name: string, maxLength = 25) => {
.substring(0, maxLength)
}
export const cleanDisplayName = (displayName: string, maxLength = 25) => {
export const cleanDisplayName = (displayName: string, maxLength = 30) => {
return displayName.replace(/\s+/g, ' ').substring(0, maxLength).trim()
}

View File

@ -12,6 +12,10 @@ export function formatMoney(amount: number) {
return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '')
}
export function formatMoneyWithDecimals(amount: number) {
return ENV_CONFIG.moneyMoniker + amount.toFixed(2)
}
export function formatWithCommas(amount: number) {
return formatter.format(Math.floor(amount)).replace('$', '')
}

View File

@ -1,3 +1,5 @@
import { sortBy, sum } from 'lodash'
export const logInterpolation = (min: number, max: number, value: number) => {
if (value <= min) return 0
if (value >= max) return 1
@ -16,4 +18,19 @@ export function normpdf(x: number, mean = 0, variance = 1) {
)
}
const TAU = Math.PI * 2
export const TAU = Math.PI * 2
export function median(xs: number[]) {
if (xs.length === 0) return NaN
const sorted = sortBy(xs, (x) => x)
const mid = Math.floor(sorted.length / 2)
if (sorted.length % 2 === 0) {
return (sorted[mid - 1] + sorted[mid]) / 2
}
return sorted[mid]
}
export function average(xs: number[]) {
return sum(xs) / xs.length
}

View File

@ -23,3 +23,18 @@ export const addObjects = <T extends { [key: string]: number }>(
return newObj as T
}
export const subtractObjects = <T extends { [key: string]: number }>(
obj1: T,
obj2: T
) => {
const keys = union(Object.keys(obj1), Object.keys(obj2))
const newObj = {} as any
for (const key of keys) {
newObj[key] = (obj1[key] ?? 0) - (obj2[key] ?? 0)
}
return newObj as T
}

View File

@ -1,3 +1,11 @@
# docs
Manifold Markets Docs
## Getting started
0. Make sure you have [Yarn 1.x][yarn]
1. `$ cd docs`
2. `$ yarn`
3. `$ yarn start`
4. The docs site will be available on http://localhost:3000

61
docs/docs/$how-to.md Normal file
View File

@ -0,0 +1,61 @@
# How to Manifold
Manifold Markets is a novel site where users can bet against each other to predict the outcomes of all types of questions. Engage in intense discussion, or joke with friends, whilst putting play-money where your mouth is.
## Mana
Mana (M$) is our virtual play currency that cannot be converted to real money.
- **Its Value**
You can redeem your Mana and we will [donate to a charity](https://manifold.markets/charity) on your behalf. Redeeming and purchasing Mana occurs at a rate of M$100 to $1. You will be able to redeem it for merch and other cool items soon too!
- **It sets us apart**
Using play-money sets us apart from other similar sites as we dont want our users to solely focus on monetary gains. Instead we prioritize providing value in the form of an enjoyable experience and facilitating a more informed world through the power of prediction markets.
## How probabilities work
The probability of a market represents what the collective bets of users predict the chances of an outcome occurring is. How this is calculated depends on the type of market - see below!
## Types of markets
There are currently 3 types of markets: Yes/No (binary), Free response, and Numerical.
- **Yes/No (Binary)**
The creator asks a question where traders can bet yes or no.
Check out [Maniswap](https://www.notion.so/Maniswap-ce406e1e897d417cbd491071ea8a0c39) for more info on its automated market maker.
- **Free Response**
The creator asks an open ended question. Both the creator and users can propose answers which can be bet on. Dont be intimidated to add new answers! The payout system and initial liquidity rewards users who bet on new answers early. The algorithm used to determine the probability and payout is complicated but if you want to learn more check out [DPM](https://www.notion.so/DPM-b9b48a09ea1f45b88d991231171730c5).
- **Numerical**
Retracted whilst we make improvements. You still may see some old ones floating around though. Questions which can be answered by a number within a given range. Betting on a value will cause you to buy shares from buckets surrounding the number you choose.
## Compete and build your portfolio
Generate profits to prove your expertise and shine above your friends.
To the moon 🚀
- **Find inaccurate probabilities**
Use your superior knowledge on topics to identify markets which have inaccurate probabilities. This gives you favorable odds, so bet accordingly to shift the probability to what you think it should be.
- **React to news**
Markets are dynamic and ongoing events can drastically affect what the probability should look like. Be the keenest to react and there is a lot of Mana to be made.
- **Buy low, sell high**
Similar to a stock market, probabilities can be overvalued and undervalued. If you bet (buy shares) at one end of the spectrum and subsequently other users buy even more shares of that same type, the value of your own shares will increase. Sometimes it will be most profitable to wait for the market to resolve but often it can be wise to sell your shares and take the immediate profits. This can also be a great way to free up Mana if you are lacking funds.
- **Create innovative answers**
Certain free response markets provide room for creativity! The answers themselves can often affect the outcome based on how compelling they are.
More questions? Check out **[this community-driven FAQ](https://docs.manifold.markets/faq)**!

View File

@ -5,7 +5,7 @@ slug: /
# About Manifold Markets
Manifold Markets lets anyone create a prediction market on any topic. Win virtual money betting on what you know, from **[chess tournaments](https://manifold.markets/SG/will-magnus-carlsen-lose-any-regula)** to **[lunar collisions](https://manifold.markets/Duncan/will-the-wayward-falcon-9-booster-h)** to **[newsletter subscriber rates](https://manifold.markets/Nu%C3%B1oSempere/how-many-additional-subscribers-wil)** - or learn about the future by creating your own market!
Manifold Markets lets anyone create a prediction market on any topic. Win virtual play money betting on what you know, from **[chess tournaments](https://manifold.markets/SG/will-magnus-carlsen-lose-any-regula)** to **[lunar collisions](https://manifold.markets/Duncan/will-the-wayward-falcon-9-booster-h)** to **[newsletter subscriber rates](https://manifold.markets/Nu%C3%B1oSempere/how-many-additional-subscribers-wil)** - or learn about the future by creating your own market!
### **What are prediction markets?**
@ -17,27 +17,13 @@ If I think the Democrats are very likely to win, and you disagree, I might offer
Now, you or I could be mistaken and overshooting the true probability one way or another. If so, there's an incentive for someone else to bet and correct it! Over time, the implied probability will converge to the **[market's best estimate](https://en.wikipedia.org/wiki/Efficient-market_hypothesis)**. Since these probabilities are public, anyone can use them to make better decisions!
### **How does Manifold Markets work?**
1. **Anyone can create a market for any yes-or-no question.**
You can ask questions about the future like "Will Taiwan remove its 14-day COVID quarantine by Jun 01, 2022?" If the market thinks this is very likely, you can plan more activities for your trip.
You can also ask subjective, personal questions like "Will I enjoy my 2022 Taiwan trip?". Then share the market with your family and friends and get their takes!
2. **Anyone can bet on a market using Manifold Dollars (M$), our platform currency.**
You get M$ 1,000 just for signing up, so you can start betting immediately! When a market creator decides an outcome in your favor, you'll win Manifold Dollars from people who bet against you.
More questions? Check out **[this community-driven FAQ](https://outsidetheasylum.blog/manifold-markets-faq/)**!
### **Can prediction markets work without real money?**
Yes! There is substantial evidence that play-money prediction markets provide real predictive power. Examples include **[sports betting](http://www.electronicmarkets.org/fileadmin/user_upload/doc/Issues/Volume_16/Issue_01/V16I1_Statistical_Tests_of_Real-Money_versus_Play-Money_Prediction_Markets.pdf)** and internal prediction markets at firms like **[Google](https://www.networkworld.com/article/2284098/google-bets-on-value-of-prediction-markets.html)**.
Our overall design also ensures that good forecasting will come out on top in the long term. In the competitive environment of the marketplace, bettors that are correct more often will gain influence, leading to better-calibrated forecasts over time.
Since our launch, we've seen hundreds of users trade each day, on over a thousand different markets! You can track the popularity of our platform at **[http://manifold.markets/analytics](http://manifold.markets/analytics)**.
Since our launch, we've seen hundreds of users trade each day, on over a thousand different markets! You can track the popularity of our platform at **[https://manifold.markets/stats](https://manifold.markets/stats)**.
### **Why is this important?**
@ -67,7 +53,7 @@ Manifold Markets is currently a team of three:
- Stephen Grugett
- Austin Chen
We've previously launched consumer-facing startups (**[Throne](https://throne.live/)**, **[One Word](http://oneword.games/platform)**), and worked at top tech and trading firms (Google, Susquehanna).
We've previously launched consumer-facing startups (**[Throne](https://throne.live/)**, **[One Word](https://oneword.games/platform)**), and worked at top tech and trading firms (Google, Susquehanna).
## **Talk to us!**

View File

@ -4,42 +4,85 @@
Our API is still in alpha — things may change or break at any time!
:::
Manifold currently supports a basic, read-only API for getting information about our markets.
If you have questions, come chat with us on [Discord](https://discord.com/invite/eHQBNBqXuh). Wed love to hear about what you build!
## List out all markets
:::
### `/v0/markets`
## General notes
Some APIs are not associated with any particular user. Other APIs require authentication.
APIs that require authentication accept an `Authorization` header in one of two formats:
- `Authorization: Key {key}`. A Manifold API key associated with a user
account. Each account may have zero or one API keys. To generate an API key
for your account, visit your user profile, click "edit", and click the
"refresh" button next to the API key field at the bottom. You can click it
again any time to invalidate your existing key and generate a new one.
- `Authorization: Bearer {jwt}`. A signed JWT from Firebase asserting your
identity. This is what our web client uses. It will probably be annoying for
you to generate and we will not document it further here.
API requests that accept parameters should either have the parameters in the
query string if they are GET requests, or have a body with a JSON object with
one property per parameter if they are POST requests.
API responses should always either have a body with a JSON result object (if
the response was a 200) or with a JSON object representing an error (if the
response was a 4xx or 5xx.)
## Endpoints
### `GET /v0/markets`
Lists all markets, ordered by creation date descending.
Parameters:
- `limit`: Optional. How many markets to return. The maximum and the default is 1000.
- `before`: Optional. The ID of the market before which the list will start. For
example, if you ask for the most recent 10 markets, and then perform a second
query for 10 more markets with `before=[the id of the 10th market]`, you will
get markets 11 through 20.
Requires no authorization.
- Example request
```
http://manifold.markets/api/v0/markets
https://manifold.markets/api/v0/markets?limit=1
```
- Example response
```json
[
{
"id":"FKtYX3t8ZfIp5gytJWAI",
"creatorUsername":"JamesGrugett",
"creatorName":"James Grugett",
"createdTime":1645139406452,
"closeTime":1647406740000,
"question":"What will be the best assessment of the Free response feature on March 15th?",
"description":"Hey guys, let's try this out!\nWe will see how people use the new Free response market type over the next month. Then I will pick the answer that I think best describes the consensus view of this feature on March 15th. Cheers.",
"id":"EvIhzcJXwhL0HavaszD7",
"creatorUsername":"Austin",
"creatorName":"Austin",
"createdTime":1653850472294,
"creatorAvatarUrl":"https://lh3.googleusercontent.com/a-/AOh14GiZyl1lBehuBMGyJYJhZd-N-mstaUtgE4xdI22lLw=s96-c",
"closeTime":1653893940000,
"question":"Will I write a new blog post today?",
"description":"I'm supposed to, or else Beeminder charges me $90.\nTentative topic ideas:\n- \"Manifold funding, a history\"\n- \"Markets and bounties allow trades through time\"\n- \"equity vs money vs time\"\n\nClose date updated to 2022-05-29 11:59 pm",
"tags":[
"ManifoldMarkets"
],
"url":"https://manifold.markets/JamesGrugett/what-will-be-the-best-assessment-of",
"pool":null,
"probability":0,
"volume7Days":100,
"volume24Hours":100,
"isResolved":false,
...
}
"personal",
"commitments"
],
"url":"https://manifold.markets/Austin/will-i-write-a-new-blog-post-today",
"pool":146.73022894879944,
"probability":0.8958175225896258,
"p":0.08281474972181882,
"totalLiquidity":102.65696071594805,
"outcomeType":"BINARY",
"mechanism":"cpmm-1",
"volume":241,
"volume7Days":0,
"volume24Hours":0,
"isResolved":true,
"resolution":"YES",
"resolutionTime":1653924077078
},
...
```
- Response type: Array of `LiteMarket`
@ -52,29 +95,47 @@ If you have questions, come chat with us on [Discord](https://discord.com/invite
// Attributes about the creator
creatorUsername: string
creatorName: string
createdTime: number
createdTime: number // milliseconds since epoch
creatorAvatarUrl?: string
// Market attributes. All times are in milliseconds since epoch
closeTime?: number // Min of creator's chosen date, and resolutionTime
question: string
description: string
// A list of tags on each market. Any user can add tags to any market.
// This list also includes the predefined categories shown as filters on the home page.
tags: string[]
// Note: This url always points to https://manifold.markets, regardless of what instance the api is running on.
// This url includes the creator's username, but this doesn't need to be correct when constructing valid URLs.
// i.e. https://manifold.markets/Austin/test-market is the same as https://manifold.markets/foo/test-market
url: string
pool: number
outcomeType: string // BINARY, FREE_RESPONSE, or NUMERIC
mechanism: string // dpm-2 or cpmm-1
probability: number
pool: { outcome: number } // For CPMM markets, the number of shares in the liquidity pool. For DPM markets, the amount of mana invested in each answer.
p?: number // CPMM markets only, probability constant in y^p * n^(1-p) = k
totalLiquidity?: number // CPMM markets only, the amount of mana deposited into the liquidity pool
volume: number
volume7Days: number
volume24Hours: number
isResolved: boolean
resolutionTime?: number
resolution?: string
resolutionProbability?: number // Used for BINARY markets resolved to MKT
}
```
## Get information about one market
### `GET /v0/market/[marketId]`
### `/v0/market/[marketId]`
Gets information about a single market by ID. Includes comments, bets, and answers.
Requires no authorization.
- Example request
@ -86,227 +147,204 @@ If you have questions, come chat with us on [Discord](https://discord.com/invite
```json
{
"id": "3zspH9sSzMlbFQLn9GKR",
"creatorUsername": "Austin",
"creatorName": "Austin Chen",
"createdTime": 1644103005345,
"closeTime": 1667894340000,
"question": "Will Carrick Flynn win the general election for Oregon's 6th District?",
"description": "The Effective Altruism movement usually stays out of politics, but here is a recent, highly-upvoted endorsement of donating to Carrick Flynn as a high-impact area: https://forum.effectivealtruism.org/posts/Qi9nnrmjwNbBqWbNT/the-best-usd5-800-i-ve-ever-donated-to-pandemic-prevention\nFurther reading: https://ballotpedia.org/Oregon%27s_6th_Congressional_District_election,_2022\n\n#EffectiveAltruism #Politics",
"tags": ["EffectiveAltruism", "Politics"],
"url": "https://manifold.markets/Austin/will-carrick-flynn-win-the-general",
"pool": 400.0916328426886,
"probability": 0.34455568984059187,
"volume7Days": 326.9083671573114,
"id": "lEoqtnDgJzft6apSKzYK",
"creatorUsername": "Angela",
"creatorName": "Angela",
"createdTime": 1655258914863,
"creatorAvatarUrl": "https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FAngela%2F50463444807_edfd4598d6_o.jpeg?alt=media&token=ef44e13b-2e6c-4498-b9c4-8e38bdaf1476",
"closeTime": 1655265001448,
"question": "What is good?",
"description": "Resolves proportionally to the answer(s) which I find most compelling. (Obviously Ill refrain from giving my own answers)\n\n(Please have at it with philosophy, ethics, etc etc)\n\n\nContract resolved automatically.",
"tags": [],
"url": "https://manifold.markets/Angela/what-is-good",
"pool": null,
"outcomeType": "FREE_RESPONSE",
"mechanism": "dpm-2",
"volume": 112,
"volume7Days": 212,
"volume24Hours": 0,
"isResolved": false,
"bets": [
"isResolved": true,
"resolution": "MKT",
"resolutionTime": 1655265001448,
"answers": [
{
"createdTime": 1644103005345,
"isAnte": true,
"shares": 83.66600265340756,
"userId": "igi2zGXsfxYPgB0DJTXVJVmwCOr2",
"amount": 70,
"probAfter": 0.3,
"probBefore": 0.3,
"id": "E1MjiVYBM0GkqRXhv5cR",
"outcome": "NO",
"contractId": "3zspH9sSzMlbFQLn9GKR"
"createdTime": 1655258941573,
"avatarUrl": "https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FAngela%2F50463444807_edfd4598d6_o.jpeg?alt=media&token=ef44e13b-2e6c-4498-b9c4-8e38bdaf1476",
"id": "1",
"username": "Angela",
"number": 1,
"name": "Angela",
"contractId": "lEoqtnDgJzft6apSKzYK",
"text": "ANTE",
"userId": "qe2QqIlOkeWsbljfeF3MsxpSJ9i2",
"probability": 0.66749733001068
},
{
"contractId": "3zspH9sSzMlbFQLn9GKR",
"probAfter": 0.3,
"shares": 54.77225575051661,
"userId": "igi2zGXsfxYPgB0DJTXVJVmwCOr2",
"isAnte": true,
"createdTime": 1644103005345,
"id": "jn3iIGwD5f0vxOHxo62o",
"amount": 30,
"probBefore": 0.3,
"outcome": "YES"
"name": "Isaac King",
"username": "IsaacKing",
"text": "This answer",
"userId": "y1hb6k7txdZPV5mgyxPFApZ7nQl2",
"id": "2",
"number": 2,
"avatarUrl": "https://lh3.googleusercontent.com/a-/AOh14GhNVriOvxK2VUAmE-jvYZwC-XIymatzVirT0Bqb2g=s96-c",
"contractId": "lEoqtnDgJzft6apSKzYK",
"createdTime": 1655261198074,
"probability": 0.008922214311142757
},
{
"shares": 11.832723364874056,
"probAfter": 0.272108843537415,
"userId": "PkBnU8cAZiOLa0fjxiUzMKsFMYZ2",
"contractId": "3zspH9sSzMlbFQLn9GKR",
"outcome": "NO",
"amount": 10,
"id": "f6sHBab6lbGw9PsnVXdc",
"probBefore": 0.3,
"createdTime": 1644203305863
"createdTime": 1655263226587,
"userId": "jbgplxty4kUKIa1MmgZk22byJq03",
"id": "3",
"avatarUrl": "https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FMartin%2Fgiphy.gif?alt=media&token=422ef610-553f-47e3-bf6f-c0c5cc16c70a",
"text": "Toyota Camry",
"contractId": "lEoqtnDgJzft6apSKzYK",
"name": "Undox",
"username": "Undox",
"number": 3,
"probability": 0.008966714133143469
},
{
"userId": "BTksWMdCeHfDitWVaAZdjLSdu3o1",
"amount": 10,
"id": "Vfui2KOQwy7gkRPP7xc6",
"shares": 18.12694184700382,
"outcome": "YES",
"createdTime": 1644212358699,
"contractId": "3zspH9sSzMlbFQLn9GKR",
"probBefore": 0.272108843537415,
"probAfter": 0.3367768595041322
},
{
"contractId": "3zspH9sSzMlbFQLn9GKR",
"probAfter": 0.3659259259259259,
"userId": "BTksWMdCeHfDitWVaAZdjLSdu3o1",
"probBefore": 0.3367768595041322,
"amount": 5,
"outcome": "YES",
"createdTime": 1644433184238,
"id": "eGI1VwAWF822LkcmOUot",
"shares": 8.435122540124937
},
{
"userId": "NHA7Gv9nNpb7b60GpLD3oFkBvPa2",
"shares": 59.79133423528123,
"amount": 50,
"probAfter": 0.24495867768595042,
"contractId": "3zspH9sSzMlbFQLn9GKR",
"createdTime": 1644693685223,
"probBefore": 0.3659259259259259,
"id": "fbU0DbmDWMnubggpQotw",
"outcome": "NO"
},
{
"amount": 25,
"userId": "iXw2OSyhs0c4QW2fAfK3yqmaYDv1",
"probAfter": 0.20583333333333328,
"outcome": "NO",
"shares": 28.3920247989266,
"contractId": "3zspH9sSzMlbFQLn9GKR",
"createdTime": 1644695698202,
"id": "k9hyljJD3MMXK2OYxTsR",
"probBefore": 0.24495867768595042
},
{
"createdTime": 1644716782308,
"shares": 11.17480183821209,
"probBefore": 0.20583333333333328,
"userId": "clvYFhVDzccYu20OUc5NBKJyDxj2",
"probAfter": 0.1927679500520291,
"id": "yYkZ4JpLgZHrRQUugpCD",
"outcome": "NO",
"contractId": "3zspH9sSzMlbFQLn9GKR",
"amount": 10
},
{
"contractId": "3zspH9sSzMlbFQLn9GKR",
"outcome": "YES",
"amount": 30,
"id": "IU2Hb1DesgKIN140BkhE",
"shares": 58.893424111838016,
"createdTime": 1644736846538,
"probBefore": 0.1927679500520291,
"userId": "BTksWMdCeHfDitWVaAZdjLSdu3o1",
"probAfter": 0.3289359861591695
},
{
"isSold": true,
"userId": "5zeWhzi9nlNNf5C9TVjshAN7QOd2",
"createdTime": 1644751343436,
"outcome": "NO",
"contractId": "3zspH9sSzMlbFQLn9GKR",
"amount": 25,
"probBefore": 0.3289359861591695,
"id": "fkCxVH7THaDbEhyJjXVk",
"probAfter": 0.2854194032651529,
"shares": 30.022082866721178
},
{
"probAfter": 0.2838618650900295,
"id": "Ao05LRRMXVWw8d7LtwhL",
"outcome": "NO",
"probBefore": 0.2854194032651529,
"shares": 1.1823269994736165,
"userId": "pUF3dMs9oLNpgU2LYtFmodaoDow1",
"amount": 1,
"contractId": "3zspH9sSzMlbFQLn9GKR",