Compare commits
1 Commits
main
...
constraine
Author | SHA1 | Date | |
---|---|---|---|
|
1bd4267773 |
27
.github/CONTRIBUTING.md
vendored
27
.github/CONTRIBUTING.md
vendored
|
@ -1,27 +0,0 @@
|
|||
# Manifold CLA
|
||||
|
||||
**Manifold Markets Contributor License Agreement**
|
||||
|
||||
(Thanks to [Beeminder](http://bmndr.co/cla) and [Discourse.org](https://cla-assistant.io/discourse/discourse) whose CLAs we modeled this on!)
|
||||
|
||||
## Unofficial Summary
|
||||
|
||||
- Manifold can use your contributions
|
||||
- Manifold can sell things involving your contributions
|
||||
- You’re legally able to agree to the above
|
||||
- You’re the one who created these contributions
|
||||
- Manifold decides what gets included in Manifold
|
||||
- Manifold does not promise any support
|
||||
|
||||
## Official Agreement
|
||||
|
||||
The document below clarifies the terms under which You (the copyright owner or legal entity authorized by the copyright owner), may make "The Contributions" (software, bug fixes, configuration changes, documentation, or any other materials) to "The Work" (Manifold Markets). This license protects You, "The Company" (Manifold Markets, Inc.) and licensees; it does not change your rights to use your own contributions for any other purpose.
|
||||
|
||||
You and "The Company" (Manifold Markets, Inc.) agree:
|
||||
|
||||
- You grant to "The Company" (Manifold Markets, Inc.) a non-exclusive, irrevocable, worldwide, royalty-free, sublicenseable, relicenseable, transferable license under all of Your relevant intellectual property rights, to use, copy, prepare derivative works of, distribute and publicly perform and display "The Contributions" on any licensing terms, including without limitation: (a) open source licenses like the GNU General Public (v2.0) license; and (b) binary, proprietary, or commercial licenses. Except for the licenses granted herein, You reserve all right, title, and interest in and to "The Contributions".
|
||||
- You grant to "The Company" a non-exclusive, irrevocable (except as stated in this section), worldwide, royalty-free, sublicenseable, transferable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer "The Work", where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with "The Work" to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or "The Work" to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
|
||||
- You are able to grant us these rights. You represent that You are legally entitled to grant the above license(s). If Your employer has rights to intellectual property that You create, You represent that You have received permission to make "The Contributions" on behalf of that employer, or that Your employer has waived such rights for "The Contributions".
|
||||
- "The Contributions" are your original work. You represent that "The Contributions" are Your original works of authorship, and to Your knowledge, no other person claims, or has the right to claim, any right in any invention or patent related to "The Contributions". You also represent that You are not legally obligated, whether by entering into an agreement or otherwise, in any way that conflicts with the terms of this license. For example, if you have signed an agreement requiring you to assign the intellectual property rights in "The Contributions" to an employer or customer, that would conflict with the terms of this license.
|
||||
- We, as authoritative representatives of "The Company" determine the code that is in "The Work". You understand that the decision to include "The Contribution(s)" in any project or source repository is entirely that of "The Company", and this agreement does not guarantee that "The Contributions" will be included in any product.
|
||||
- No Implied Warranties. "The Company" acknowledges that, except as explicitly described in this Agreement, the Contribution is provided on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
|
55
.github/workflows/check.yml
vendored
55
.github/workflows/check.yml
vendored
|
@ -1,55 +0,0 @@
|
|||
name: Check PRs
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 3
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Static analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Restore cached node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Install missing dependencies
|
||||
run: yarn install --prefer-offline --frozen-lockfile
|
||||
- name: Run Prettier on web client
|
||||
working-directory: web
|
||||
run: npx prettier --check .
|
||||
- name: Run ESLint on common
|
||||
if: ${{ success() || failure() }}
|
||||
working-directory: common
|
||||
run: npx eslint . --max-warnings 0
|
||||
- name: Run ESLint on web client
|
||||
if: ${{ success() || failure() }}
|
||||
working-directory: web
|
||||
run: yarn lint --max-warnings 0
|
||||
- name: Run ESLint on cloud functions
|
||||
if: ${{ success() || failure() }}
|
||||
working-directory: functions
|
||||
run: npx eslint . --max-warnings 0
|
||||
- name: Run Typescript checker on web client
|
||||
if: ${{ success() || failure() }}
|
||||
working-directory: web
|
||||
run: tsc --pretty --project tsconfig.json --noEmit
|
||||
- name: Run Typescript checker on cloud functions
|
||||
if: ${{ success() || failure() }}
|
||||
working-directory: functions
|
||||
run: tsc -b -v --pretty
|
43
.github/workflows/format.yml
vendored
43
.github/workflows/format.yml
vendored
|
@ -1,43 +0,0 @@
|
|||
name: Reformat main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 3
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
|
||||
# mqp - i generated a personal token to use for these writes -- it's unclear
|
||||
# why, but the default token didn't work, even when i gave it max permissions
|
||||
|
||||
jobs:
|
||||
prettify:
|
||||
name: Auto-prettify
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.FORMATTER_ACCESS_TOKEN }}
|
||||
- name: Restore cached node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Install missing dependencies
|
||||
run: yarn install --prefer-offline --frozen-lockfile
|
||||
- name: Run Prettier on web client
|
||||
working-directory: web
|
||||
run: yarn format
|
||||
- name: Commit any Prettier changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: Auto-prettification
|
||||
branch: ${{ github.head_ref }}
|
43
.github/workflows/lint.yml
vendored
43
.github/workflows/lint.yml
vendored
|
@ -1,43 +0,0 @@
|
|||
name: Run linter (remove unused imports)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 3
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
|
||||
# mqp - i generated a personal token to use for these writes -- it's unclear
|
||||
# why, but the default token didn't work, even when i gave it max permissions
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Auto-lint
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.FORMATTER_ACCESS_TOKEN }}
|
||||
- name: Restore cached node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Install missing dependencies
|
||||
run: yarn install --prefer-offline --frozen-lockfile
|
||||
- name: Run lint script
|
||||
run: yarn lint
|
||||
- name: Commit any lint changes
|
||||
if: always()
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: Auto-remove unused imports
|
||||
branch: ${{ github.head_ref }}
|
17
.github/workflows/merge-main-into-main2.yml
vendored
17
.github/workflows/merge-main-into-main2.yml
vendored
|
@ -1,17 +0,0 @@
|
|||
name: Merge main into main2 on every commit
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
jobs:
|
||||
merge-branch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Merge main -> main2
|
||||
uses: devmasx/merge-branch@master
|
||||
with:
|
||||
type: now
|
||||
target_branch: main2
|
||||
github_token: ${{ github.token }}
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,6 +2,3 @@
|
|||
.idea/
|
||||
.vercel
|
||||
node_modules
|
||||
yarn-error.log
|
||||
|
||||
firebase-debug.log
|
14
.vscode/extensions.json
vendored
14
.vscode/extensions.json
vendored
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
// 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": []
|
||||
}
|
16
.vscode/launch.json
vendored
16
.vscode/launch.json
vendored
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach to Chrome",
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"port": 9222, // chrome needs to be started with the parameter "--remote-debugging-port=9222"
|
||||
"urlFilter": "http://localhost:3000/*",
|
||||
"webRoot": "${workspaceFolder}/web"
|
||||
}
|
||||
]
|
||||
}
|
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"javascript.preferences.importModuleSpecifier": "shortest",
|
||||
"typescript.preferences.importModuleSpecifier": "shortest",
|
||||
"files.eol": "\n",
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/package-lock.json": true,
|
||||
"**/yarn.lock": true
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
|
@ -31,15 +31,13 @@ Operations with complicated contracts (e.g. buying shares) are provided in a sep
|
|||
|
||||
- `og-image/`: The OpenGraph image generator; creates the preview images shown on Twitter/social media.
|
||||
|
||||
- `docs/`: Manifold's public documentation that lives at https://docs.manifold.markets.
|
||||
Also: Our docs are currently in [a separate repo](https://github.com/manifoldmarkets/docs). TODO: move them into this monorepo.
|
||||
|
||||
## Contributing
|
||||
|
||||
Since we are just now open-sourcing things, we will see how things go. Feel free to open issues, submit PRs, and chat about the process on [Discord][discord]. We would prefer [small PRs][small-prs] that we can effectively evaluate and review -- maybe check in with us first if you are thinking to work on a big change.
|
||||
|
||||
By contributing to this codebase, you are agreeing to the terms of the [Manifold CLA](https://github.com/manifoldmarkets/manifold/blob/main/.github/CONTRIBUTING.md).
|
||||
|
||||
If you need additional access to any infrastructure in order to work on something (e.g. Vercel, Firebase) let us know about that on [Discord][discord] as well.
|
||||
If you need additional access to any infrastructure in order to work on something (e.g. Vercel, Firebase) let us know about that on Discord as well.
|
||||
|
||||
[vercel]: https://vercel.com/
|
||||
[jamstack]: https://jamstack.org/
|
||||
|
@ -50,4 +48,4 @@ If you need additional access to any infrastructure in order to work on somethin
|
|||
[cloud-firestore]: https://firebase.google.com/docs/firestore
|
||||
[cloud-functions]: https://firebase.google.com/docs/functions
|
||||
[small-prs]: https://google.github.io/eng-practices/review/developer/small-cls.html
|
||||
[discord]: https://discord.gg/3Zuth9792G
|
||||
[discord]: https://discord.gg/eHQBNBqXuh
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: ['lodash', 'unused-imports'],
|
||||
extends: ['eslint:recommended'],
|
||||
ignorePatterns: ['lib'],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: ['plugin:@typescript-eslint/recommended'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
'no-extra-semi': 'off',
|
||||
'no-constant-condition': ['error', { checkLoops: false }],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'lodash/import-scope': [2, 'member'],
|
||||
},
|
||||
}
|
5
common/.gitignore
vendored
5
common/.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
# Compiled JavaScript files
|
||||
lib/
|
||||
lib/**/*.js
|
||||
lib/**/*.js.map
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
@ -9,4 +10,4 @@ node_modules/
|
|||
|
||||
package-lock.json
|
||||
ui-debug.log
|
||||
firebase-debug.log
|
||||
firebase-debug.log
|
|
@ -1 +0,0 @@
|
|||
save-prefix ""
|
|
@ -1,30 +1,35 @@
|
|||
import { getCpmmLiquidity } from './calculate-cpmm'
|
||||
import { CPMMContract } from './contract'
|
||||
import { addCpmmLiquidity, getCpmmLiquidity } from './calculate-cpmm'
|
||||
import { Binary, CPMM, FullContract } from './contract'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import { User } from './user'
|
||||
|
||||
export const getNewLiquidityProvision = (
|
||||
userId: string,
|
||||
user: User,
|
||||
amount: number,
|
||||
contract: CPMMContract,
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
newLiquidityProvisionId: string
|
||||
) => {
|
||||
const { pool, p, totalLiquidity, subsidyPool } = contract
|
||||
const { pool, p, totalLiquidity } = contract
|
||||
|
||||
const liquidity = getCpmmLiquidity(pool, p)
|
||||
const { newPool, newP } = addCpmmLiquidity(pool, p, amount)
|
||||
|
||||
const liquidity =
|
||||
getCpmmLiquidity(newPool, newP) - getCpmmLiquidity(pool, newP)
|
||||
|
||||
const newLiquidityProvision: LiquidityProvision = {
|
||||
id: newLiquidityProvisionId,
|
||||
userId: userId,
|
||||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
pool,
|
||||
p,
|
||||
pool: newPool,
|
||||
p: newP,
|
||||
liquidity,
|
||||
createdTime: Date.now(),
|
||||
}
|
||||
|
||||
const newTotalLiquidity = (totalLiquidity ?? 0) + amount
|
||||
const newSubsidyPool = (subsidyPool ?? 0) + amount
|
||||
|
||||
return { newLiquidityProvision, newTotalLiquidity, newSubsidyPool }
|
||||
const newBalance = user.balance - amount
|
||||
|
||||
return { newLiquidityProvision, newPool, newP, newBalance, newTotalLiquidity }
|
||||
}
|
||||
|
|
128
common/antes.ts
128
common/antes.ts
|
@ -1,30 +1,19 @@
|
|||
import { range } from 'lodash'
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import { getDpmProbability, getValueFromBucket } from './calculate-dpm'
|
||||
import {
|
||||
CPMMBinaryContract,
|
||||
DPMBinaryContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
NumericContract,
|
||||
} from './contract'
|
||||
import { Bet } from './bet'
|
||||
import { getDpmProbability } from './calculate-dpm'
|
||||
import { Binary, CPMM, DPM, FreeResponse, FullContract } from './contract'
|
||||
import { User } from './user'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import { noFees } from './fees'
|
||||
import { Answer } from './answer'
|
||||
|
||||
export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id
|
||||
export const DEV_HOUSE_LIQUIDITY_PROVIDER_ID = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // @ManifoldMarkets' id
|
||||
export const UNIQUE_BETTOR_LIQUIDITY_AMOUNT = 20
|
||||
export const FIXED_ANTE = 50
|
||||
|
||||
type NormalizedBet<T extends Bet = Bet> = Omit<
|
||||
T,
|
||||
'userAvatarUrl' | 'userName' | 'userUsername'
|
||||
>
|
||||
// deprecated
|
||||
export const PHANTOM_ANTE = 0.001
|
||||
export const MINIMUM_ANTE = 50
|
||||
|
||||
export function getCpmmInitialLiquidity(
|
||||
providerId: string,
|
||||
contract: CPMMBinaryContract,
|
||||
creator: User,
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
anteId: string,
|
||||
amount: number
|
||||
) {
|
||||
|
@ -32,7 +21,7 @@ export function getCpmmInitialLiquidity(
|
|||
|
||||
const lp: LiquidityProvision = {
|
||||
id: anteId,
|
||||
userId: providerId,
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
createdTime,
|
||||
isAnte: true,
|
||||
|
@ -48,7 +37,7 @@ export function getCpmmInitialLiquidity(
|
|||
|
||||
export function getAnteBets(
|
||||
creator: User,
|
||||
contract: DPMBinaryContract,
|
||||
contract: FullContract<DPM, Binary>,
|
||||
yesAnteId: string,
|
||||
noAnteId: string
|
||||
) {
|
||||
|
@ -57,7 +46,7 @@ export function getAnteBets(
|
|||
|
||||
const { createdTime } = contract
|
||||
|
||||
const yesBet: NormalizedBet = {
|
||||
const yesBet: Bet = {
|
||||
id: yesAnteId,
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
|
@ -71,7 +60,7 @@ export function getAnteBets(
|
|||
fees: noFees,
|
||||
}
|
||||
|
||||
const noBet: NormalizedBet = {
|
||||
const noBet: Bet = {
|
||||
id: noAnteId,
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
|
@ -89,8 +78,8 @@ export function getAnteBets(
|
|||
}
|
||||
|
||||
export function getFreeAnswerAnte(
|
||||
anteBettorId: string,
|
||||
contract: FreeResponseContract,
|
||||
creator: User,
|
||||
contract: FullContract<DPM, FreeResponse>,
|
||||
anteBetId: string
|
||||
) {
|
||||
const { totalBets, totalShares } = contract
|
||||
|
@ -99,9 +88,9 @@ export function getFreeAnswerAnte(
|
|||
|
||||
const { createdTime } = contract
|
||||
|
||||
const anteBet: NormalizedBet = {
|
||||
const anteBet: Bet = {
|
||||
id: anteBetId,
|
||||
userId: anteBettorId,
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
shares,
|
||||
|
@ -115,86 +104,3 @@ export function getFreeAnswerAnte(
|
|||
|
||||
return anteBet
|
||||
}
|
||||
|
||||
export function getMultipleChoiceAntes(
|
||||
creator: User,
|
||||
contract: MultipleChoiceContract,
|
||||
answers: string[],
|
||||
betDocIds: string[]
|
||||
) {
|
||||
const { totalBets, totalShares } = contract
|
||||
const amount = totalBets['0']
|
||||
const shares = totalShares['0']
|
||||
const p = 1 / answers.length
|
||||
|
||||
const { createdTime } = contract
|
||||
|
||||
const bets: NormalizedBet[] = answers.map((answer, i) => ({
|
||||
id: betDocIds[i],
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
shares,
|
||||
outcome: i.toString(),
|
||||
probBefore: p,
|
||||
probAfter: p,
|
||||
createdTime,
|
||||
isAnte: true,
|
||||
fees: noFees,
|
||||
}))
|
||||
|
||||
const { username, name, avatarUrl } = creator
|
||||
|
||||
const answerObjects: Answer[] = answers.map((answer, i) => ({
|
||||
id: i.toString(),
|
||||
number: i,
|
||||
contractId: contract.id,
|
||||
createdTime,
|
||||
userId: creator.id,
|
||||
username,
|
||||
name,
|
||||
avatarUrl,
|
||||
text: answer,
|
||||
}))
|
||||
|
||||
return { bets, answerObjects }
|
||||
}
|
||||
|
||||
export function getNumericAnte(
|
||||
anteBettorId: string,
|
||||
contract: NumericContract,
|
||||
ante: number,
|
||||
newBetId: string
|
||||
) {
|
||||
const { bucketCount, createdTime } = contract
|
||||
|
||||
const betAnte = ante / bucketCount
|
||||
const betShares = Math.sqrt(ante ** 2 / bucketCount)
|
||||
|
||||
const allOutcomeShares = Object.fromEntries(
|
||||
range(0, bucketCount).map((_, i) => [i, betShares])
|
||||
)
|
||||
|
||||
const allBetAmounts = Object.fromEntries(
|
||||
range(0, bucketCount).map((_, i) => [i, betAnte])
|
||||
)
|
||||
|
||||
const anteBet: NormalizedBet<NumericBet> = {
|
||||
id: newBetId,
|
||||
userId: anteBettorId,
|
||||
contractId: contract.id,
|
||||
amount: ante,
|
||||
allBetAmounts,
|
||||
outcome: '0',
|
||||
value: getValueFromBucket('0', contract),
|
||||
shares: betShares,
|
||||
allOutcomeShares,
|
||||
probBefore: 0,
|
||||
probAfter: 1 / bucketCount,
|
||||
createdTime,
|
||||
isAnte: true,
|
||||
fees: noFees,
|
||||
}
|
||||
|
||||
return anteBet
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { ENV_CONFIG } from './envs/constants'
|
||||
|
||||
export class APIError extends Error {
|
||||
code: number
|
||||
details?: unknown
|
||||
constructor(code: number, message: string, details?: unknown) {
|
||||
super(message)
|
||||
this.code = code
|
||||
this.name = 'APIError'
|
||||
this.details = details
|
||||
}
|
||||
}
|
||||
|
||||
export function getFunctionUrl(name: string) {
|
||||
if (process.env.NEXT_PUBLIC_FUNCTIONS_URL) {
|
||||
return `${process.env.NEXT_PUBLIC_FUNCTIONS_URL}/${name}`
|
||||
} else if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
|
||||
const { projectId, region } = ENV_CONFIG.firebaseConfig
|
||||
return `http://localhost:5001/${projectId}/${region}/${name}`
|
||||
} else {
|
||||
const { cloudRunId, cloudRunRegion } = ENV_CONFIG
|
||||
return `https://${name}-${cloudRunId}-${cloudRunRegion}.a.run.app`
|
||||
}
|
||||
}
|
123
common/badge.ts
123
common/badge.ts
|
@ -1,123 +0,0 @@
|
|||
import { User } from './user'
|
||||
|
||||
export type Badge = {
|
||||
type: BadgeTypes
|
||||
createdTime: number
|
||||
data: { [key: string]: any }
|
||||
name: 'Proven Correct' | 'Streaker' | 'Market Creator'
|
||||
}
|
||||
|
||||
export type BadgeTypes = 'PROVEN_CORRECT' | 'STREAKER' | 'MARKET_CREATOR'
|
||||
|
||||
export type ProvenCorrectBadgeData = {
|
||||
type: 'PROVEN_CORRECT'
|
||||
data: {
|
||||
contractSlug: string
|
||||
contractCreatorUsername: string
|
||||
contractTitle: string
|
||||
commentId: string
|
||||
betAmount: number
|
||||
}
|
||||
}
|
||||
|
||||
export type MarketCreatorBadgeData = {
|
||||
type: 'MARKET_CREATOR'
|
||||
data: {
|
||||
totalContractsCreated: number
|
||||
}
|
||||
}
|
||||
|
||||
export type StreakerBadgeData = {
|
||||
type: 'STREAKER'
|
||||
data: {
|
||||
totalBettingStreak: number
|
||||
}
|
||||
}
|
||||
|
||||
export type ProvenCorrectBadge = Badge & ProvenCorrectBadgeData
|
||||
export type StreakerBadge = Badge & StreakerBadgeData
|
||||
export type MarketCreatorBadge = Badge & MarketCreatorBadgeData
|
||||
|
||||
export const MINIMUM_UNIQUE_BETTORS_FOR_PROVEN_CORRECT_BADGE = 5
|
||||
export const provenCorrectRarityThresholds = [1, 1000, 10000]
|
||||
const calculateProvenCorrectBadgeRarity = (badge: ProvenCorrectBadge) => {
|
||||
const { betAmount } = badge.data
|
||||
const thresholdArray = provenCorrectRarityThresholds
|
||||
let i = thresholdArray.length - 1
|
||||
while (i >= 0) {
|
||||
if (betAmount >= thresholdArray[i]) {
|
||||
return i + 1
|
||||
}
|
||||
i--
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
export const streakerBadgeRarityThresholds = [1, 50, 250]
|
||||
const calculateStreakerBadgeRarity = (badge: StreakerBadge) => {
|
||||
const { totalBettingStreak } = badge.data
|
||||
const thresholdArray = streakerBadgeRarityThresholds
|
||||
let i = thresholdArray.length - 1
|
||||
while (i >= 0) {
|
||||
if (totalBettingStreak == thresholdArray[i]) {
|
||||
return i + 1
|
||||
}
|
||||
i--
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
export const marketCreatorBadgeRarityThresholds = [1, 75, 300]
|
||||
const calculateMarketCreatorBadgeRarity = (badge: MarketCreatorBadge) => {
|
||||
const { totalContractsCreated } = badge.data
|
||||
const thresholdArray = marketCreatorBadgeRarityThresholds
|
||||
let i = thresholdArray.length - 1
|
||||
while (i >= 0) {
|
||||
if (totalContractsCreated == thresholdArray[i]) {
|
||||
return i + 1
|
||||
}
|
||||
i--
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
export type rarities = 'bronze' | 'silver' | 'gold'
|
||||
|
||||
const rarityRanks: { [key: number]: rarities } = {
|
||||
1: 'bronze',
|
||||
2: 'silver',
|
||||
3: 'gold',
|
||||
}
|
||||
|
||||
export const calculateBadgeRarity = (badge: Badge) => {
|
||||
switch (badge.type) {
|
||||
case 'PROVEN_CORRECT':
|
||||
return rarityRanks[
|
||||
calculateProvenCorrectBadgeRarity(badge as ProvenCorrectBadge)
|
||||
]
|
||||
case 'MARKET_CREATOR':
|
||||
return rarityRanks[
|
||||
calculateMarketCreatorBadgeRarity(badge as MarketCreatorBadge)
|
||||
]
|
||||
case 'STREAKER':
|
||||
return rarityRanks[calculateStreakerBadgeRarity(badge as StreakerBadge)]
|
||||
default:
|
||||
return rarityRanks[0]
|
||||
}
|
||||
}
|
||||
|
||||
export const getBadgesByRarity = (user: User | null | undefined) => {
|
||||
const rarities: { [key in rarities]: number } = {
|
||||
bronze: 0,
|
||||
silver: 0,
|
||||
gold: 0,
|
||||
}
|
||||
if (!user) return rarities
|
||||
Object.values(user.achievements).map((value) => {
|
||||
value.badges.map((badge) => {
|
||||
rarities[calculateBadgeRarity(badge)] =
|
||||
(rarities[calculateBadgeRarity(badge)] ?? 0) + 1
|
||||
})
|
||||
})
|
||||
return rarities
|
||||
}
|
|
@ -3,14 +3,7 @@ import { Fees } from './fees'
|
|||
export type Bet = {
|
||||
id: string
|
||||
userId: string
|
||||
|
||||
// denormalized for bet lists
|
||||
userAvatarUrl?: string
|
||||
userUsername: string
|
||||
userName: string
|
||||
|
||||
contractId: string
|
||||
createdTime: number
|
||||
|
||||
amount: number // bet size; negative if SELL bet
|
||||
loanAmount?: number
|
||||
|
@ -20,50 +13,20 @@ export type Bet = {
|
|||
probBefore: number
|
||||
probAfter: number
|
||||
|
||||
sale?: {
|
||||
amount: number // amount user makes from sale
|
||||
betId: string // id of bet being sold
|
||||
// TODO: add sale time?
|
||||
}
|
||||
|
||||
fees: Fees
|
||||
|
||||
isSold?: boolean // true if this BUY bet has been sold
|
||||
isAnte?: boolean
|
||||
isLiquidityProvision?: boolean
|
||||
isRedemption?: boolean
|
||||
challengeSlug?: string
|
||||
|
||||
// Props for bets in DPM contract below.
|
||||
// A bet is either a BUY or a SELL that sells all of a previous buy.
|
||||
isSold?: boolean // true if this BUY bet has been sold
|
||||
// This field marks a SELL bet.
|
||||
sale?: {
|
||||
amount: number // amount user makes from sale
|
||||
betId: string // id of BUY bet being sold
|
||||
}
|
||||
} & Partial<LimitProps>
|
||||
|
||||
export type NumericBet = Bet & {
|
||||
value: number
|
||||
allOutcomeShares: { [outcome: string]: number }
|
||||
allBetAmounts: { [outcome: string]: number }
|
||||
createdTime: number
|
||||
}
|
||||
|
||||
// Binary market limit order.
|
||||
export type LimitBet = Bet & LimitProps
|
||||
|
||||
type LimitProps = {
|
||||
orderAmount: number // Amount of limit order.
|
||||
limitProb: number // [0, 1]. Bet to this probability.
|
||||
isFilled: boolean // Whether all of the bet amount has been filled.
|
||||
isCancelled: boolean // Whether to prevent any further fills.
|
||||
// A record of each transaction that partially (or fully) fills the orderAmount.
|
||||
// I.e. A limit order could be filled by partially matching with several bets.
|
||||
// Non-limit orders can also be filled by matching with multiple limit orders.
|
||||
fills: fill[]
|
||||
}
|
||||
|
||||
export type fill = {
|
||||
// The id the bet matched against, or null if the bet was matched by the pool.
|
||||
matchedBetId: string | null
|
||||
amount: number
|
||||
shares: number
|
||||
timestamp: number
|
||||
// If the fill is a sale, it means the matching bet has shares of the same outcome.
|
||||
// I.e. -fill.shares === matchedBet.shares
|
||||
isSale?: boolean
|
||||
}
|
||||
export const MAX_LOAN_PER_CONTRACT = 20
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
import { groupBy, mapValues, sumBy } from 'lodash'
|
||||
import { LimitBet } from './bet'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import { computeFills } from './new-bet'
|
||||
import { binarySearch } from './util/algos'
|
||||
|
||||
export type CpmmState = {
|
||||
pool: { [outcome: string]: number }
|
||||
p: number
|
||||
}
|
||||
import { Binary, CPMM, FullContract } from './contract'
|
||||
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, noFees, PLATFORM_FEE } from './fees'
|
||||
|
||||
export function getCpmmProbability(
|
||||
pool: { [outcome: string]: number },
|
||||
|
@ -20,11 +12,11 @@ export function getCpmmProbability(
|
|||
}
|
||||
|
||||
export function getCpmmProbabilityAfterBetBeforeFees(
|
||||
state: CpmmState,
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
outcome: string,
|
||||
bet: number
|
||||
) {
|
||||
const { pool, p } = state
|
||||
const { pool, p } = contract
|
||||
const shares = calculateCpmmShares(pool, p, bet, outcome)
|
||||
const { YES: y, NO: n } = pool
|
||||
|
||||
|
@ -37,12 +29,12 @@ export function getCpmmProbabilityAfterBetBeforeFees(
|
|||
}
|
||||
|
||||
export function getCpmmOutcomeProbabilityAfterBet(
|
||||
state: CpmmState,
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
outcome: string,
|
||||
bet: number
|
||||
) {
|
||||
const { newPool } = calculateCpmmPurchase(state, bet, outcome)
|
||||
const p = getCpmmProbability(newPool, state.p)
|
||||
const { newPool } = calculateCpmmPurchase(contract, bet, outcome)
|
||||
const p = getCpmmProbability(newPool, contract.p)
|
||||
return outcome === 'NO' ? 1 - p : p
|
||||
}
|
||||
|
||||
|
@ -64,8 +56,12 @@ function calculateCpmmShares(
|
|||
: n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p))
|
||||
}
|
||||
|
||||
export function getCpmmFees(state: CpmmState, bet: number, outcome: string) {
|
||||
const prob = getCpmmProbabilityAfterBetBeforeFees(state, outcome, bet)
|
||||
export function getCpmmLiquidityFee(
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
bet: number,
|
||||
outcome: string
|
||||
) {
|
||||
const prob = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet)
|
||||
const betP = outcome === 'YES' ? 1 - prob : prob
|
||||
|
||||
const liquidityFee = LIQUIDITY_FEE * betP * bet
|
||||
|
@ -76,27 +72,29 @@ export function getCpmmFees(state: CpmmState, bet: number, outcome: string) {
|
|||
const totalFees = liquidityFee + platformFee + creatorFee
|
||||
const remainingBet = bet - totalFees
|
||||
|
||||
return { remainingBet, totalFees, fees }
|
||||
return { remainingBet, fees }
|
||||
}
|
||||
|
||||
export function calculateCpmmSharesAfterFee(
|
||||
state: CpmmState,
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
bet: number,
|
||||
outcome: string
|
||||
) {
|
||||
const { pool, p } = state
|
||||
const { remainingBet } = getCpmmFees(state, bet, outcome)
|
||||
const { pool, p } = contract
|
||||
const { remainingBet } = getCpmmLiquidityFee(contract, bet, outcome)
|
||||
|
||||
return calculateCpmmShares(pool, p, remainingBet, outcome)
|
||||
}
|
||||
|
||||
export function calculateCpmmPurchase(
|
||||
state: CpmmState,
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
bet: number,
|
||||
outcome: string
|
||||
) {
|
||||
const { pool, p } = state
|
||||
const { remainingBet, fees } = getCpmmFees(state, bet, outcome)
|
||||
const { pool, p } = contract
|
||||
const { remainingBet, fees } = getCpmmLiquidityFee(contract, bet, outcome)
|
||||
// const remainingBet = bet
|
||||
// const fees = noFees
|
||||
|
||||
const shares = calculateCpmmShares(pool, p, remainingBet, outcome)
|
||||
const { YES: y, NO: n } = pool
|
||||
|
@ -115,125 +113,119 @@ export function calculateCpmmPurchase(
|
|||
return { shares, newPool, newP, fees }
|
||||
}
|
||||
|
||||
// Note: there might be a closed form solution for this.
|
||||
// If so, feel free to switch out this implementation.
|
||||
export function calculateCpmmAmountToProb(
|
||||
state: CpmmState,
|
||||
prob: number,
|
||||
outcome: 'YES' | 'NO'
|
||||
) {
|
||||
if (prob <= 0 || prob >= 1 || isNaN(prob)) return Infinity
|
||||
if (outcome === 'NO') prob = 1 - prob
|
||||
|
||||
// First, find an upper bound that leads to a more extreme probability than prob.
|
||||
let maxGuess = 10
|
||||
let newProb = 0
|
||||
do {
|
||||
maxGuess *= 10
|
||||
newProb = getCpmmOutcomeProbabilityAfterBet(state, outcome, maxGuess)
|
||||
} while (newProb < prob)
|
||||
|
||||
// Then, binary search for the amount that gets closest to prob.
|
||||
const amount = binarySearch(0, maxGuess, (amount) => {
|
||||
const newProb = getCpmmOutcomeProbabilityAfterBet(state, outcome, amount)
|
||||
return newProb - prob
|
||||
})
|
||||
|
||||
return amount
|
||||
function computeK(y: number, n: number, p: number) {
|
||||
return y ** p * n ** (1 - p)
|
||||
}
|
||||
|
||||
function calculateAmountToBuyShares(
|
||||
state: CpmmState,
|
||||
shares: number,
|
||||
function sellSharesK(
|
||||
y: number,
|
||||
n: number,
|
||||
p: number,
|
||||
s: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
b: number
|
||||
) {
|
||||
// Search for amount between bounds (0, shares).
|
||||
// Min share price is M$0, and max is M$1 each.
|
||||
return binarySearch(0, shares, (amount) => {
|
||||
const { takers } = computeFills(
|
||||
outcome,
|
||||
amount,
|
||||
state,
|
||||
undefined,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
return outcome === 'YES'
|
||||
? computeK(y - b + s, n - b, p)
|
||||
: computeK(y - b, n - b + s, p)
|
||||
}
|
||||
|
||||
const totalShares = sumBy(takers, (taker) => taker.shares)
|
||||
return totalShares - shares
|
||||
})
|
||||
function calculateCpmmShareValue(
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO'
|
||||
) {
|
||||
const { pool, p } = contract
|
||||
|
||||
// Find bet amount that preserves k after selling shares.
|
||||
const k = computeK(pool.YES, pool.NO, p)
|
||||
const otherPool = outcome === 'YES' ? pool.NO : pool.YES
|
||||
|
||||
// Constrain the max sale value to the lessor of 1. shares and 2. the other pool.
|
||||
// This is because 1. the max value per share is M$ 1,
|
||||
// and 2. The other pool cannot go negative and the sale value is subtracted from it.
|
||||
// (Without this, there are multiple solutions for the same k.)
|
||||
let highAmount = Math.min(shares, otherPool)
|
||||
let lowAmount = 0
|
||||
let mid = 0
|
||||
let kGuess = 0
|
||||
while (true) {
|
||||
mid = lowAmount + (highAmount - lowAmount) / 2
|
||||
|
||||
// Break once we've reached max precision.
|
||||
if (mid === lowAmount || mid === highAmount) break
|
||||
|
||||
kGuess = sellSharesK(pool.YES, pool.NO, p, shares, outcome, mid)
|
||||
if (kGuess < k) {
|
||||
highAmount = mid
|
||||
} else {
|
||||
lowAmount = mid
|
||||
}
|
||||
}
|
||||
return mid
|
||||
}
|
||||
|
||||
export function calculateCpmmSale(
|
||||
state: CpmmState,
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
outcome: string
|
||||
) {
|
||||
if (Math.round(shares) < 0) {
|
||||
if (shares < 0) {
|
||||
throw new Error('Cannot sell non-positive shares')
|
||||
}
|
||||
|
||||
const oppositeOutcome = outcome === 'YES' ? 'NO' : 'YES'
|
||||
const buyAmount = calculateAmountToBuyShares(
|
||||
state,
|
||||
const saleValue = calculateCpmmShareValue(
|
||||
contract,
|
||||
shares,
|
||||
oppositeOutcome,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
outcome as 'YES' | 'NO'
|
||||
)
|
||||
|
||||
const { cpmmState, makers, takers, totalFees, ordersToCancel } = computeFills(
|
||||
oppositeOutcome,
|
||||
buyAmount,
|
||||
state,
|
||||
undefined,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
const fees = noFees
|
||||
|
||||
// Transform buys of opposite outcome into sells.
|
||||
const saleTakers = takers.map((taker) => ({
|
||||
...taker,
|
||||
// You bought opposite shares, which combine with existing shares, removing them.
|
||||
shares: -taker.shares,
|
||||
// Opposite shares combine with shares you are selling for M$ of shares.
|
||||
// You paid taker.amount for the opposite shares.
|
||||
// Take the negative because this is money you gain.
|
||||
amount: -(taker.shares - taker.amount),
|
||||
isSale: true,
|
||||
}))
|
||||
// const { fees, remainingBet: saleValue } = getCpmmLiquidityFee(
|
||||
// contract,
|
||||
// rawSaleValue,
|
||||
// outcome === 'YES' ? 'NO' : 'YES'
|
||||
// )
|
||||
|
||||
const saleValue = -sumBy(saleTakers, (taker) => taker.amount)
|
||||
const { pool } = contract
|
||||
const { YES: y, NO: n } = pool
|
||||
|
||||
return {
|
||||
saleValue,
|
||||
cpmmState,
|
||||
fees: totalFees,
|
||||
makers,
|
||||
takers: saleTakers,
|
||||
ordersToCancel,
|
||||
const { liquidityFee: fee } = fees
|
||||
|
||||
const [newY, newN] =
|
||||
outcome === 'YES'
|
||||
? [y + shares - saleValue + fee, n - saleValue + fee]
|
||||
: [y - saleValue + fee, n + shares - saleValue + fee]
|
||||
|
||||
if (newY < 0 || newN < 0) {
|
||||
console.log('calculateCpmmSale', {
|
||||
newY,
|
||||
newN,
|
||||
y,
|
||||
n,
|
||||
shares,
|
||||
saleValue,
|
||||
fee,
|
||||
outcome,
|
||||
})
|
||||
throw new Error('Cannot sell more than in pool')
|
||||
}
|
||||
|
||||
const postBetPool = { YES: newY, NO: newN }
|
||||
|
||||
const { newPool, newP } = addCpmmLiquidity(postBetPool, contract.p, fee)
|
||||
|
||||
return { saleValue, newPool, newP, fees }
|
||||
}
|
||||
|
||||
export function getCpmmProbabilityAfterSale(
|
||||
state: CpmmState,
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
outcome: 'YES' | 'NO'
|
||||
) {
|
||||
const { cpmmState } = calculateCpmmSale(
|
||||
state,
|
||||
shares,
|
||||
outcome,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
return getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
const { newPool } = calculateCpmmSale(contract, shares, outcome)
|
||||
return getCpmmProbability(newPool, contract.p)
|
||||
}
|
||||
|
||||
export function getCpmmLiquidity(
|
||||
|
@ -266,23 +258,22 @@ export function addCpmmLiquidity(
|
|||
return { newPool, liquidity, newP }
|
||||
}
|
||||
|
||||
export function getCpmmLiquidityPoolWeights(liquidities: LiquidityProvision[]) {
|
||||
const userAmounts = groupBy(liquidities, (w) => w.userId)
|
||||
const totalAmount = sumBy(liquidities, (w) => w.amount)
|
||||
// export function removeCpmmLiquidity(
|
||||
// contract: FullContract<CPMM, Binary>,
|
||||
// liquidity: number
|
||||
// ) {
|
||||
// const { YES, NO } = contract.pool
|
||||
// const poolLiquidity = getCpmmLiquidity({ YES, NO })
|
||||
// const p = getCpmmProbability({ YES, NO }, contract.p)
|
||||
|
||||
return mapValues(
|
||||
userAmounts,
|
||||
(amounts) => sumBy(amounts, (w) => w.amount) / totalAmount
|
||||
)
|
||||
}
|
||||
// const f = liquidity / poolLiquidity
|
||||
// const [payoutYes, payoutNo] = [f * YES, f * NO]
|
||||
|
||||
export function getUserLiquidityShares(
|
||||
userId: string,
|
||||
state: CpmmState,
|
||||
liquidities: LiquidityProvision[]
|
||||
) {
|
||||
const weights = getCpmmLiquidityPoolWeights(liquidities)
|
||||
const userWeight = weights[userId] ?? 0
|
||||
// const betAmount = Math.abs(payoutYes - payoutNo)
|
||||
// const betOutcome = p >= 0.5 ? 'NO' : 'YES' // opposite side as adding liquidity
|
||||
// const payout = Math.min(payoutYes, payoutNo)
|
||||
|
||||
return mapValues(state.pool, (shares) => userWeight * shares)
|
||||
}
|
||||
// const newPool = { YES: YES - payoutYes, NO: NO - payoutNo }
|
||||
|
||||
// return { newPool, payout, betAmount, betOutcome }
|
||||
// }
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { cloneDeep, range, sum, sumBy, sortBy, mapValues } from 'lodash'
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import { DPMContract, DPMBinaryContract, NumericContract } from './contract'
|
||||
import * as _ from 'lodash'
|
||||
import { Bet } from './bet'
|
||||
import { Binary, DPM, FreeResponse, FullContract } from './contract'
|
||||
import { DPM_FEES } from './fees'
|
||||
import { normpdf } from './util/math'
|
||||
import { addObjects } from './util/object'
|
||||
|
||||
export function getDpmProbability(totalShares: { [outcome: string]: number }) {
|
||||
// For binary contracts only.
|
||||
|
@ -16,94 +14,11 @@ export function getDpmOutcomeProbability(
|
|||
},
|
||||
outcome: string
|
||||
) {
|
||||
const squareSum = sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||
const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||
const shares = totalShares[outcome] ?? 0
|
||||
return shares ** 2 / squareSum
|
||||
}
|
||||
|
||||
export function getDpmOutcomeProbabilities(totalShares: {
|
||||
[outcome: string]: number
|
||||
}) {
|
||||
const squareSum = sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||
return mapValues(totalShares, (shares) => shares ** 2 / squareSum)
|
||||
}
|
||||
|
||||
export function getNumericBets(
|
||||
contract: NumericContract,
|
||||
bucket: string,
|
||||
betAmount: number,
|
||||
variance: number
|
||||
) {
|
||||
const { bucketCount } = contract
|
||||
const bucketNumber = parseInt(bucket)
|
||||
const buckets = range(0, bucketCount)
|
||||
|
||||
const mean = bucketNumber / bucketCount
|
||||
|
||||
const allDensities = buckets.map((i) =>
|
||||
normpdf(i / bucketCount, mean, variance)
|
||||
)
|
||||
const densitySum = sum(allDensities)
|
||||
|
||||
const rawBetAmounts = allDensities
|
||||
.map((d) => (d / densitySum) * betAmount)
|
||||
.map((x) => (x >= 1 / bucketCount ? x : 0))
|
||||
|
||||
const rawSum = sum(rawBetAmounts)
|
||||
const scaledBetAmounts = rawBetAmounts.map((x) => (x / rawSum) * betAmount)
|
||||
|
||||
const bets = scaledBetAmounts
|
||||
.map((x, i) => (x > 0 ? [i.toString(), x] : undefined))
|
||||
.filter((x) => x != undefined) as [string, number][]
|
||||
|
||||
return bets
|
||||
}
|
||||
|
||||
export const getMappedBucket = (value: number, contract: NumericContract) => {
|
||||
const { bucketCount, min, max } = contract
|
||||
|
||||
const index = Math.floor(((value - min) / (max - min)) * bucketCount)
|
||||
const bucket = Math.max(Math.min(index, bucketCount - 1), 0)
|
||||
|
||||
return `${bucket}`
|
||||
}
|
||||
|
||||
export const getValueFromBucket = (
|
||||
bucket: string,
|
||||
contract: NumericContract
|
||||
) => {
|
||||
const { bucketCount, min, max } = contract
|
||||
const index = parseInt(bucket)
|
||||
const value = min + (index / bucketCount) * (max - min)
|
||||
const rounded = Math.round(value * 1e4) / 1e4
|
||||
return rounded
|
||||
}
|
||||
|
||||
export const getExpectedValue = (contract: NumericContract) => {
|
||||
const { bucketCount, min, max, totalShares } = contract
|
||||
|
||||
const totalShareSum = sumBy(
|
||||
Object.values(totalShares),
|
||||
(shares) => shares ** 2
|
||||
)
|
||||
const probs = range(0, bucketCount).map(
|
||||
(i) => totalShares[i] ** 2 / totalShareSum
|
||||
)
|
||||
|
||||
const values = range(0, bucketCount).map(
|
||||
(i) =>
|
||||
// use mid point within bucket
|
||||
0.5 * (min + (i / bucketCount) * (max - min)) +
|
||||
0.5 * (min + ((i + 1) / bucketCount) * (max - min))
|
||||
)
|
||||
|
||||
const weightedValues = range(0, bucketCount).map((i) => probs[i] * values[i])
|
||||
|
||||
const expectation = sum(weightedValues)
|
||||
const rounded = Math.round(expectation * 1e2) / 1e2
|
||||
return rounded
|
||||
}
|
||||
|
||||
export function getDpmOutcomeProbabilityAfterBet(
|
||||
totalShares: {
|
||||
[outcome: string]: number
|
||||
|
@ -140,7 +55,7 @@ export function calculateDpmShares(
|
|||
bet: number,
|
||||
betChoice: string
|
||||
) {
|
||||
const squareSum = sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||
const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||
const shares = totalShares[betChoice] ?? 0
|
||||
|
||||
const c = 2 * bet * Math.sqrt(squareSum)
|
||||
|
@ -148,30 +63,6 @@ export function calculateDpmShares(
|
|||
return Math.sqrt(bet ** 2 + shares ** 2 + c) - shares
|
||||
}
|
||||
|
||||
export function calculateNumericDpmShares(
|
||||
totalShares: {
|
||||
[outcome: string]: number
|
||||
},
|
||||
bets: [string, number][]
|
||||
) {
|
||||
const shares: number[] = []
|
||||
|
||||
totalShares = cloneDeep(totalShares)
|
||||
|
||||
const order = sortBy(
|
||||
bets.map(([, amount], i) => [amount, i]),
|
||||
([amount]) => amount
|
||||
).map(([, i]) => i)
|
||||
|
||||
for (const i of order) {
|
||||
const [bucket, bet] = bets[i]
|
||||
shares[i] = calculateDpmShares(totalShares, bet, bucket)
|
||||
totalShares = addObjects(totalShares, { [bucket]: shares[i] })
|
||||
}
|
||||
|
||||
return { shares, totalShares }
|
||||
}
|
||||
|
||||
export function calculateDpmRawShareValue(
|
||||
totalShares: {
|
||||
[outcome: string]: number
|
||||
|
@ -180,11 +71,11 @@ export function calculateDpmRawShareValue(
|
|||
betChoice: string
|
||||
) {
|
||||
const currentValue = Math.sqrt(
|
||||
sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||
_.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||
)
|
||||
|
||||
const postSaleValue = Math.sqrt(
|
||||
sumBy(Object.keys(totalShares), (outcome) =>
|
||||
_.sumBy(Object.keys(totalShares), (outcome) =>
|
||||
outcome === betChoice
|
||||
? Math.max(0, totalShares[outcome] - shares) ** 2
|
||||
: totalShares[outcome] ** 2
|
||||
|
@ -195,7 +86,7 @@ export function calculateDpmRawShareValue(
|
|||
}
|
||||
|
||||
export function calculateDpmMoneyRatio(
|
||||
contract: DPMContract,
|
||||
contract: FullContract<DPM, any>,
|
||||
bet: Bet,
|
||||
shareValue: number
|
||||
) {
|
||||
|
@ -204,12 +95,12 @@ export function calculateDpmMoneyRatio(
|
|||
|
||||
const p = getDpmOutcomeProbability(totalShares, outcome)
|
||||
|
||||
const actual = sum(Object.values(pool)) - shareValue
|
||||
const actual = _.sum(Object.values(pool)) - shareValue
|
||||
|
||||
const betAmount = p * amount
|
||||
|
||||
const expected =
|
||||
sumBy(
|
||||
_.sumBy(
|
||||
Object.keys(totalBets),
|
||||
(outcome) =>
|
||||
getDpmOutcomeProbability(totalShares, outcome) *
|
||||
|
@ -221,7 +112,10 @@ export function calculateDpmMoneyRatio(
|
|||
return actual / expected
|
||||
}
|
||||
|
||||
export function calculateDpmShareValue(contract: DPMContract, bet: Bet) {
|
||||
export function calculateDpmShareValue(
|
||||
contract: FullContract<DPM, any>,
|
||||
bet: Bet
|
||||
) {
|
||||
const { pool, totalShares } = contract
|
||||
const { shares, outcome } = bet
|
||||
|
||||
|
@ -233,14 +127,17 @@ export function calculateDpmShareValue(contract: DPMContract, bet: Bet) {
|
|||
return adjShareValue
|
||||
}
|
||||
|
||||
export function calculateDpmSaleAmount(contract: DPMContract, bet: Bet) {
|
||||
export function calculateDpmSaleAmount(
|
||||
contract: FullContract<DPM, any>,
|
||||
bet: Bet
|
||||
) {
|
||||
const { amount } = bet
|
||||
const winnings = calculateDpmShareValue(contract, bet)
|
||||
return deductDpmFees(amount, winnings)
|
||||
}
|
||||
|
||||
export function calculateDpmPayout(
|
||||
contract: DPMContract,
|
||||
contract: FullContract<DPM, any>,
|
||||
bet: Bet,
|
||||
outcome: string
|
||||
) {
|
||||
|
@ -250,52 +147,43 @@ export function calculateDpmPayout(
|
|||
return calculateStandardDpmPayout(contract, bet, outcome)
|
||||
}
|
||||
|
||||
export function calculateDpmCancelPayout(contract: DPMContract, bet: Bet) {
|
||||
export function calculateDpmCancelPayout(
|
||||
contract: FullContract<DPM, any>,
|
||||
bet: Bet
|
||||
) {
|
||||
const { totalBets, pool } = contract
|
||||
const betTotal = sum(Object.values(totalBets))
|
||||
const poolTotal = sum(Object.values(pool))
|
||||
const betTotal = _.sum(Object.values(totalBets))
|
||||
const poolTotal = _.sum(Object.values(pool))
|
||||
|
||||
return (bet.amount / betTotal) * poolTotal
|
||||
}
|
||||
|
||||
export function calculateStandardDpmPayout(
|
||||
contract: DPMContract,
|
||||
contract: FullContract<DPM, any>,
|
||||
bet: Bet,
|
||||
outcome: string
|
||||
) {
|
||||
const { outcome: betOutcome } = bet
|
||||
const isNumeric = contract.outcomeType === 'NUMERIC'
|
||||
if (!isNumeric && betOutcome !== outcome) return 0
|
||||
|
||||
const shares = isNumeric
|
||||
? ((bet as NumericBet).allOutcomeShares ?? {})[outcome]
|
||||
: bet.shares
|
||||
|
||||
if (!shares) return 0
|
||||
const { amount, outcome: betOutcome, shares } = bet
|
||||
if (betOutcome !== outcome) return 0
|
||||
|
||||
const { totalShares, phantomShares, pool } = contract
|
||||
if (!totalShares[outcome]) return 0
|
||||
|
||||
const poolTotal = sum(Object.values(pool))
|
||||
const poolTotal = _.sum(Object.values(pool))
|
||||
|
||||
const total =
|
||||
totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0)
|
||||
|
||||
const winnings = (shares / total) * poolTotal
|
||||
|
||||
const amount = isNumeric
|
||||
? (bet as NumericBet).allBetAmounts[outcome]
|
||||
: bet.amount
|
||||
|
||||
const payout = amount + (1 - DPM_FEES) * Math.max(0, winnings - amount)
|
||||
return payout
|
||||
// profit can be negative if using phantom shares
|
||||
return amount + (1 - DPM_FEES) * Math.max(0, winnings - amount)
|
||||
}
|
||||
|
||||
export function calculateDpmPayoutAfterCorrectBet(
|
||||
contract: DPMContract,
|
||||
contract: FullContract<DPM, any>,
|
||||
bet: Bet
|
||||
) {
|
||||
const { totalShares, pool, totalBets, outcomeType } = contract
|
||||
const { totalShares, pool, totalBets } = contract
|
||||
const { shares, amount, outcome } = bet
|
||||
|
||||
const prevShares = totalShares[outcome] ?? 0
|
||||
|
@ -316,57 +204,52 @@ export function calculateDpmPayoutAfterCorrectBet(
|
|||
...totalBets,
|
||||
[outcome]: prevTotalBet + amount,
|
||||
},
|
||||
outcomeType:
|
||||
outcomeType === 'NUMERIC'
|
||||
? 'FREE_RESPONSE' // hack to show payout at particular bet point estimate
|
||||
: outcomeType,
|
||||
}
|
||||
|
||||
return calculateStandardDpmPayout(newContract as any, bet, outcome)
|
||||
return calculateStandardDpmPayout(newContract, bet, outcome)
|
||||
}
|
||||
|
||||
function calculateMktDpmPayout(contract: DPMContract, bet: Bet) {
|
||||
function calculateMktDpmPayout(contract: FullContract<DPM, any>, bet: Bet) {
|
||||
if (contract.outcomeType === 'BINARY')
|
||||
return calculateBinaryMktDpmPayout(contract, bet)
|
||||
|
||||
const { totalShares, pool, resolutions, outcomeType } = contract
|
||||
const { totalShares, pool, resolutions } = contract as FullContract<
|
||||
DPM,
|
||||
FreeResponse
|
||||
>
|
||||
|
||||
let probs: { [outcome: string]: number }
|
||||
|
||||
if (resolutions) {
|
||||
const probTotal = sum(Object.values(resolutions))
|
||||
probs = mapValues(
|
||||
const probTotal = _.sum(Object.values(resolutions))
|
||||
probs = _.mapValues(
|
||||
totalShares,
|
||||
(_, outcome) => (resolutions[outcome] ?? 0) / probTotal
|
||||
)
|
||||
} else {
|
||||
const squareSum = sum(
|
||||
const squareSum = _.sum(
|
||||
Object.values(totalShares).map((shares) => shares ** 2)
|
||||
)
|
||||
probs = mapValues(totalShares, (shares) => shares ** 2 / squareSum)
|
||||
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 =
|
||||
outcomeType === 'NUMERIC'
|
||||
? sumBy(
|
||||
Object.keys((bet as NumericBet).allOutcomeShares ?? {}),
|
||||
(outcome) => {
|
||||
return (
|
||||
(probs[outcome] * (bet as NumericBet).allOutcomeShares[outcome]) /
|
||||
totalShares[outcome]
|
||||
)
|
||||
}
|
||||
)
|
||||
: (probs[outcome] * shares) / totalShares[outcome]
|
||||
|
||||
const totalPool = sum(Object.values(pool))
|
||||
const totalPool = _.sum(Object.values(pool))
|
||||
const poolFrac = (probs[outcome] * shares) / weightedShareTotal
|
||||
const winnings = poolFrac * totalPool
|
||||
|
||||
return deductDpmFees(amount, winnings)
|
||||
}
|
||||
|
||||
function calculateBinaryMktDpmPayout(contract: DPMBinaryContract, bet: Bet) {
|
||||
function calculateBinaryMktDpmPayout(
|
||||
contract: FullContract<DPM, Binary>,
|
||||
bet: Bet
|
||||
) {
|
||||
const { resolutionProbability, totalShares, phantomShares } = contract
|
||||
const p =
|
||||
resolutionProbability !== undefined
|
||||
|
@ -387,7 +270,7 @@ function calculateBinaryMktDpmPayout(contract: DPMBinaryContract, bet: Bet) {
|
|||
return deductDpmFees(amount, winnings)
|
||||
}
|
||||
|
||||
export function resolvedDpmPayout(contract: DPMContract, bet: Bet) {
|
||||
export function resolvedDpmPayout(contract: FullContract<DPM, any>, bet: Bet) {
|
||||
if (contract.resolution)
|
||||
return calculateDpmPayout(contract, bet, contract.resolution)
|
||||
throw new Error('Contract was not resolved')
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Bet } from './bet'
|
||||
import { getProbability } from './calculate'
|
||||
import { CPMMContract } from './contract'
|
||||
import { Binary, FixedPayouts, FullContract } from './contract'
|
||||
|
||||
export function calculateFixedPayout(
|
||||
contract: CPMMContract,
|
||||
contract: FullContract<FixedPayouts, Binary>,
|
||||
bet: Bet,
|
||||
outcome: string
|
||||
) {
|
||||
|
@ -23,7 +23,10 @@ export function calculateStandardFixedPayout(bet: Bet, outcome: string) {
|
|||
return shares
|
||||
}
|
||||
|
||||
function calculateFixedMktPayout(contract: CPMMContract, bet: Bet) {
|
||||
function calculateFixedMktPayout(
|
||||
contract: FullContract<FixedPayouts, Binary>,
|
||||
bet: Bet
|
||||
) {
|
||||
const { resolutionProbability } = contract
|
||||
const p =
|
||||
resolutionProbability !== undefined
|
||||
|
|
|
@ -1,315 +0,0 @@
|
|||
import { Dictionary, groupBy, last, partition, sum, sumBy, uniq } from 'lodash'
|
||||
import { calculatePayout, getContractBetMetrics } from './calculate'
|
||||
import { Bet, LimitBet } from './bet'
|
||||
import {
|
||||
Contract,
|
||||
CPMMBinaryContract,
|
||||
CPMMContract,
|
||||
DPMContract,
|
||||
} from './contract'
|
||||
import { PortfolioMetrics, User } from './user'
|
||||
import { DAY_MS } from './util/time'
|
||||
import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet'
|
||||
import { getCpmmProbability } from './calculate-cpmm'
|
||||
import { removeUndefinedProps } from './util/object'
|
||||
|
||||
const computeInvestmentValue = (
|
||||
bets: Bet[],
|
||||
contractsDict: { [k: string]: Contract }
|
||||
) => {
|
||||
return sumBy(bets, (bet) => {
|
||||
const contract = contractsDict[bet.contractId]
|
||||
if (!contract || contract.isResolved) return 0
|
||||
if (bet.sale || bet.isSold) return 0
|
||||
|
||||
const payout = calculatePayout(contract, bet, 'MKT')
|
||||
const value = payout - (bet.loanAmount ?? 0)
|
||||
if (isNaN(value)) return 0
|
||||
return value
|
||||
})
|
||||
}
|
||||
|
||||
export const computeInvestmentValueCustomProb = (
|
||||
bets: Bet[],
|
||||
contract: Contract,
|
||||
p: number
|
||||
) => {
|
||||
return sumBy(bets, (bet) => {
|
||||
if (!contract || contract.isResolved) return 0
|
||||
if (bet.sale || bet.isSold) return 0
|
||||
const { outcome, shares } = bet
|
||||
|
||||
const betP = outcome === 'YES' ? p : 1 - p
|
||||
|
||||
const value = betP * shares
|
||||
if (isNaN(value)) return 0
|
||||
return value
|
||||
})
|
||||
}
|
||||
|
||||
export const computeElasticity = (
|
||||
bets: Bet[],
|
||||
contract: Contract,
|
||||
betAmount = 50
|
||||
) => {
|
||||
const { mechanism, outcomeType } = contract
|
||||
return mechanism === 'cpmm-1' &&
|
||||
(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC')
|
||||
? computeBinaryCpmmElasticity(bets, contract, betAmount)
|
||||
: computeDpmElasticity(contract, betAmount)
|
||||
}
|
||||
|
||||
export const computeBinaryCpmmElasticity = (
|
||||
bets: Bet[],
|
||||
contract: CPMMContract,
|
||||
betAmount: number
|
||||
) => {
|
||||
const limitBets = bets
|
||||
.filter(
|
||||
(b) =>
|
||||
!b.isFilled &&
|
||||
!b.isSold &&
|
||||
!b.isRedemption &&
|
||||
!b.sale &&
|
||||
!b.isCancelled &&
|
||||
b.limitProb !== undefined
|
||||
)
|
||||
.sort((a, b) => a.createdTime - b.createdTime) as LimitBet[]
|
||||
|
||||
const userIds = uniq(limitBets.map((b) => b.userId))
|
||||
// Assume all limit orders are good.
|
||||
const userBalances = Object.fromEntries(
|
||||
userIds.map((id) => [id, Number.MAX_SAFE_INTEGER])
|
||||
)
|
||||
|
||||
const { newPool: poolY, newP: pY } = getBinaryCpmmBetInfo(
|
||||
'YES',
|
||||
betAmount,
|
||||
contract,
|
||||
undefined,
|
||||
limitBets,
|
||||
userBalances
|
||||
)
|
||||
const resultYes = getCpmmProbability(poolY, pY)
|
||||
|
||||
const { newPool: poolN, newP: pN } = getBinaryCpmmBetInfo(
|
||||
'NO',
|
||||
betAmount,
|
||||
contract,
|
||||
undefined,
|
||||
limitBets,
|
||||
userBalances
|
||||
)
|
||||
const resultNo = getCpmmProbability(poolN, pN)
|
||||
|
||||
// handle AMM overflow
|
||||
const safeYes = Number.isFinite(resultYes) ? resultYes : 1
|
||||
const safeNo = Number.isFinite(resultNo) ? resultNo : 0
|
||||
|
||||
return safeYes - safeNo
|
||||
}
|
||||
|
||||
export const computeDpmElasticity = (
|
||||
contract: DPMContract,
|
||||
betAmount: number
|
||||
) => {
|
||||
return getNewMultiBetInfo('', 2 * betAmount, contract).newBet.probAfter
|
||||
}
|
||||
|
||||
const computeTotalPool = (userContracts: Contract[], startTime = 0) => {
|
||||
const periodFilteredContracts = userContracts.filter(
|
||||
(contract) => contract.createdTime >= startTime
|
||||
)
|
||||
return sum(
|
||||
periodFilteredContracts.map((contract) => sum(Object.values(contract.pool)))
|
||||
)
|
||||
}
|
||||
|
||||
export const computeVolume = (contractBets: Bet[], since: number) => {
|
||||
return sumBy(contractBets, (b) =>
|
||||
b.createdTime > since && !b.isRedemption ? Math.abs(b.amount) : 0
|
||||
)
|
||||
}
|
||||
|
||||
const calculateProbChangeSince = (descendingBets: Bet[], since: number) => {
|
||||
const newestBet = descendingBets[0]
|
||||
if (!newestBet) return 0
|
||||
|
||||
const betBeforeSince = descendingBets.find((b) => b.createdTime < since)
|
||||
|
||||
if (!betBeforeSince) {
|
||||
const oldestBet = last(descendingBets) ?? newestBet
|
||||
return newestBet.probAfter - oldestBet.probBefore
|
||||
}
|
||||
|
||||
return newestBet.probAfter - betBeforeSince.probAfter
|
||||
}
|
||||
|
||||
export const calculateProbChanges = (descendingBets: Bet[]) => {
|
||||
const now = Date.now()
|
||||
const yesterday = now - DAY_MS
|
||||
const weekAgo = now - 7 * DAY_MS
|
||||
const monthAgo = now - 30 * DAY_MS
|
||||
|
||||
return {
|
||||
day: calculateProbChangeSince(descendingBets, yesterday),
|
||||
week: calculateProbChangeSince(descendingBets, weekAgo),
|
||||
month: calculateProbChangeSince(descendingBets, monthAgo),
|
||||
}
|
||||
}
|
||||
|
||||
export const calculateCreatorVolume = (userContracts: Contract[]) => {
|
||||
const allTimeCreatorVolume = computeTotalPool(userContracts, 0)
|
||||
const monthlyCreatorVolume = computeTotalPool(
|
||||
userContracts,
|
||||
Date.now() - 30 * DAY_MS
|
||||
)
|
||||
const weeklyCreatorVolume = computeTotalPool(
|
||||
userContracts,
|
||||
Date.now() - 7 * DAY_MS
|
||||
)
|
||||
|
||||
const dailyCreatorVolume = computeTotalPool(
|
||||
userContracts,
|
||||
Date.now() - 1 * DAY_MS
|
||||
)
|
||||
|
||||
return {
|
||||
daily: dailyCreatorVolume,
|
||||
weekly: weeklyCreatorVolume,
|
||||
monthly: monthlyCreatorVolume,
|
||||
allTime: allTimeCreatorVolume,
|
||||
}
|
||||
}
|
||||
|
||||
export const calculateNewPortfolioMetrics = (
|
||||
user: User,
|
||||
contractsById: { [k: string]: Contract },
|
||||
currentBets: Bet[]
|
||||
) => {
|
||||
const investmentValue = computeInvestmentValue(currentBets, contractsById)
|
||||
const newPortfolio = {
|
||||
investmentValue: investmentValue,
|
||||
balance: user.balance,
|
||||
totalDeposits: user.totalDeposits,
|
||||
timestamp: Date.now(),
|
||||
userId: user.id,
|
||||
}
|
||||
return newPortfolio
|
||||
}
|
||||
|
||||
const calculateProfitForPeriod = (
|
||||
startingPortfolio: PortfolioMetrics | undefined,
|
||||
currentProfit: number
|
||||
) => {
|
||||
if (startingPortfolio === undefined) {
|
||||
return currentProfit
|
||||
}
|
||||
|
||||
const startingProfit = calculatePortfolioProfit(startingPortfolio)
|
||||
|
||||
return currentProfit - startingProfit
|
||||
}
|
||||
|
||||
export const calculatePortfolioProfit = (portfolio: PortfolioMetrics) => {
|
||||
return portfolio.investmentValue + portfolio.balance - portfolio.totalDeposits
|
||||
}
|
||||
|
||||
export const calculateNewProfit = (
|
||||
portfolioHistory: Record<
|
||||
'current' | 'day' | 'week' | 'month',
|
||||
PortfolioMetrics | undefined
|
||||
>,
|
||||
newPortfolio: PortfolioMetrics
|
||||
) => {
|
||||
const allTimeProfit = calculatePortfolioProfit(newPortfolio)
|
||||
|
||||
const newProfit = {
|
||||
daily: calculateProfitForPeriod(portfolioHistory.day, allTimeProfit),
|
||||
weekly: calculateProfitForPeriod(portfolioHistory.week, allTimeProfit),
|
||||
monthly: calculateProfitForPeriod(portfolioHistory.month, allTimeProfit),
|
||||
allTime: allTimeProfit,
|
||||
}
|
||||
|
||||
return newProfit
|
||||
}
|
||||
|
||||
export const calculateMetricsByContract = (
|
||||
bets: Bet[],
|
||||
contractsById: Dictionary<Contract>
|
||||
) => {
|
||||
const betsByContract = groupBy(bets, (bet) => bet.contractId)
|
||||
const unresolvedContracts = Object.keys(betsByContract)
|
||||
.map((cid) => contractsById[cid])
|
||||
.filter((c) => c && !c.isResolved)
|
||||
|
||||
return unresolvedContracts.map((c) => {
|
||||
const bets = betsByContract[c.id] ?? []
|
||||
const current = getContractBetMetrics(c, bets)
|
||||
|
||||
let periodMetrics
|
||||
if (c.mechanism === 'cpmm-1' && c.outcomeType === 'BINARY') {
|
||||
const periods = ['day', 'week', 'month'] as const
|
||||
periodMetrics = Object.fromEntries(
|
||||
periods.map((period) => [
|
||||
period,
|
||||
calculatePeriodProfit(c, bets, period),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
return removeUndefinedProps({
|
||||
contractId: c.id,
|
||||
...current,
|
||||
from: periodMetrics,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export type ContractMetrics = ReturnType<
|
||||
typeof calculateMetricsByContract
|
||||
>[number]
|
||||
|
||||
const calculatePeriodProfit = (
|
||||
contract: CPMMBinaryContract,
|
||||
bets: Bet[],
|
||||
period: 'day' | 'week' | 'month'
|
||||
) => {
|
||||
const days = period === 'day' ? 1 : period === 'week' ? 7 : 30
|
||||
const fromTime = Date.now() - days * DAY_MS
|
||||
const [previousBets, recentBets] = partition(
|
||||
bets,
|
||||
(b) => b.createdTime < fromTime
|
||||
)
|
||||
|
||||
const prevProb = contract.prob - contract.probChanges[period]
|
||||
const prob = contract.resolutionProbability
|
||||
? contract.resolutionProbability
|
||||
: contract.prob
|
||||
|
||||
const previousBetsValue = computeInvestmentValueCustomProb(
|
||||
previousBets,
|
||||
contract,
|
||||
prevProb
|
||||
)
|
||||
const currentBetsValue = computeInvestmentValueCustomProb(
|
||||
previousBets,
|
||||
contract,
|
||||
prob
|
||||
)
|
||||
|
||||
const { profit: recentProfit, invested: recentInvested } =
|
||||
getContractBetMetrics(contract, recentBets)
|
||||
|
||||
const profit = currentBetsValue - previousBetsValue + recentProfit
|
||||
const invested = previousBetsValue + recentInvested
|
||||
const profitPercent = invested === 0 ? 0 : 100 * (profit / invested)
|
||||
|
||||
return {
|
||||
profit,
|
||||
profitPercent,
|
||||
invested,
|
||||
prevValue: previousBetsValue,
|
||||
value: currentBetsValue,
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { maxBy, partition, sortBy, sum, sumBy } from 'lodash'
|
||||
import { Bet, LimitBet } from './bet'
|
||||
import * as _ from 'lodash'
|
||||
import { Bet } from './bet'
|
||||
import {
|
||||
calculateCpmmSale,
|
||||
getCpmmProbability,
|
||||
|
@ -19,24 +19,22 @@ import {
|
|||
} from './calculate-dpm'
|
||||
import { calculateFixedPayout } from './calculate-fixed-payouts'
|
||||
import {
|
||||
Binary,
|
||||
Contract,
|
||||
BinaryContract,
|
||||
CPMM,
|
||||
DPM,
|
||||
FreeResponseContract,
|
||||
PseudoNumericContract,
|
||||
MultipleChoiceContract,
|
||||
FullContract,
|
||||
} from './contract'
|
||||
import { floatingEqual } from './util/math'
|
||||
|
||||
export function getProbability(
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
) {
|
||||
export function getProbability(contract: FullContract<DPM | CPMM, Binary>) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
? getCpmmProbability(contract.pool, contract.p)
|
||||
: getDpmProbability(contract.totalShares)
|
||||
}
|
||||
|
||||
export function getInitialProbability(
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
contract: FullContract<DPM | CPMM, Binary>
|
||||
) {
|
||||
if (contract.initialProbability) return contract.initialProbability
|
||||
|
||||
|
@ -61,7 +59,11 @@ export function getOutcomeProbabilityAfterBet(
|
|||
bet: number
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
? getCpmmOutcomeProbabilityAfterBet(contract, outcome, bet)
|
||||
? getCpmmOutcomeProbabilityAfterBet(
|
||||
contract as FullContract<CPMM, Binary>,
|
||||
outcome,
|
||||
bet
|
||||
)
|
||||
: getDpmOutcomeProbabilityAfterBet(contract.totalShares, outcome, bet)
|
||||
}
|
||||
|
||||
|
@ -71,26 +73,17 @@ export function calculateShares(
|
|||
betChoice: string
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
? calculateCpmmSharesAfterFee(contract, bet, betChoice)
|
||||
? calculateCpmmSharesAfterFee(
|
||||
contract as FullContract<CPMM, Binary>,
|
||||
bet,
|
||||
betChoice
|
||||
)
|
||||
: calculateDpmShares(contract.totalShares, bet, betChoice)
|
||||
}
|
||||
|
||||
export function calculateSaleAmount(
|
||||
contract: Contract,
|
||||
bet: Bet,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
? calculateCpmmSale(
|
||||
contract,
|
||||
Math.abs(bet.shares),
|
||||
bet.outcome as 'YES' | 'NO',
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
).saleValue
|
||||
export function calculateSaleAmount(contract: Contract, bet: Bet) {
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue
|
||||
: calculateDpmSaleAmount(contract, bet)
|
||||
}
|
||||
|
||||
|
@ -103,25 +96,19 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
|
|||
export function getProbabilityAfterSale(
|
||||
contract: Contract,
|
||||
outcome: string,
|
||||
shares: number,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
shares: number
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
? getCpmmProbabilityAfterSale(
|
||||
contract,
|
||||
contract as FullContract<CPMM, Binary>,
|
||||
shares,
|
||||
outcome as 'YES' | 'NO',
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
outcome as 'YES' | 'NO'
|
||||
)
|
||||
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
|
||||
}
|
||||
|
||||
export function calculatePayout(contract: Contract, bet: Bet, outcome: string) {
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
? calculateFixedPayout(contract, bet, outcome)
|
||||
: calculateDpmPayout(contract, bet, outcome)
|
||||
}
|
||||
|
@ -130,72 +117,23 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
|
|||
const outcome = contract.resolution
|
||||
if (!outcome) throw new Error('Contract not resolved')
|
||||
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
return contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY'
|
||||
? calculateFixedPayout(contract, bet, outcome)
|
||||
: calculateDpmPayout(contract, bet, outcome)
|
||||
}
|
||||
|
||||
function getCpmmInvested(yourBets: Bet[]) {
|
||||
const totalShares: { [outcome: string]: number } = {}
|
||||
const totalSpent: { [outcome: string]: number } = {}
|
||||
|
||||
const sortedBets = sortBy(yourBets, 'createdTime')
|
||||
for (const bet of sortedBets) {
|
||||
const { outcome, shares, amount } = bet
|
||||
if (floatingEqual(shares, 0)) continue
|
||||
|
||||
const spent = totalSpent[outcome] ?? 0
|
||||
const position = totalShares[outcome] ?? 0
|
||||
|
||||
if (amount > 0) {
|
||||
totalShares[outcome] = position + shares
|
||||
totalSpent[outcome] = spent + amount
|
||||
} else if (amount < 0) {
|
||||
const averagePrice = position === 0 ? 0 : spent / position
|
||||
totalShares[outcome] = position + shares
|
||||
totalSpent[outcome] = spent + averagePrice * shares
|
||||
}
|
||||
}
|
||||
|
||||
return sum([0, ...Object.values(totalSpent)])
|
||||
}
|
||||
|
||||
function getDpmInvested(yourBets: Bet[]) {
|
||||
const sortedBets = sortBy(yourBets, 'createdTime')
|
||||
|
||||
return sumBy(sortedBets, (bet) => {
|
||||
const { amount, sale } = bet
|
||||
|
||||
if (sale) {
|
||||
const originalBet = sortedBets.find((b) => b.id === sale.betId)
|
||||
if (originalBet) return -originalBet.amount
|
||||
return 0
|
||||
}
|
||||
|
||||
return amount
|
||||
})
|
||||
}
|
||||
|
||||
export type ContractBetMetrics = ReturnType<typeof getContractBetMetrics>
|
||||
|
||||
export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
||||
const { resolution } = contract
|
||||
const isCpmm = contract.mechanism === 'cpmm-1'
|
||||
|
||||
let currentInvested = 0
|
||||
let totalInvested = 0
|
||||
let payout = 0
|
||||
let loan = 0
|
||||
let saleValue = 0
|
||||
let redeemed = 0
|
||||
const totalShares: { [outcome: string]: number } = {}
|
||||
|
||||
for (const bet of yourBets) {
|
||||
const { isSold, sale, amount, loanAmount, isRedemption, shares, outcome } =
|
||||
bet
|
||||
totalShares[outcome] = (totalShares[outcome] ?? 0) + shares
|
||||
|
||||
const { isSold, sale, amount, loanAmount, isRedemption } = bet
|
||||
if (isSold) {
|
||||
totalInvested += amount
|
||||
} else if (sale) {
|
||||
|
@ -209,6 +147,7 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
saleValue -= amount
|
||||
}
|
||||
|
||||
currentInvested += amount
|
||||
loan += loanAmount ?? 0
|
||||
payout += resolution
|
||||
? calculatePayout(contract, bet, resolution)
|
||||
|
@ -216,42 +155,32 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
}
|
||||
}
|
||||
|
||||
const netPayout = payout - loan
|
||||
const profit = payout + saleValue + redeemed - totalInvested
|
||||
const profitPercent = totalInvested === 0 ? 0 : (profit / totalInvested) * 100
|
||||
|
||||
const invested = isCpmm ? getCpmmInvested(yourBets) : getDpmInvested(yourBets)
|
||||
const hasShares = Object.values(totalShares).some(
|
||||
(shares) => !floatingEqual(shares, 0)
|
||||
)
|
||||
const profitPercent = (profit / totalInvested) * 100
|
||||
|
||||
return {
|
||||
invested,
|
||||
loan,
|
||||
invested: Math.max(0, currentInvested),
|
||||
payout,
|
||||
netPayout,
|
||||
profit,
|
||||
profitPercent,
|
||||
totalShares,
|
||||
hasShares,
|
||||
}
|
||||
}
|
||||
|
||||
export function getContractBetNullMetrics() {
|
||||
return {
|
||||
invested: 0,
|
||||
loan: 0,
|
||||
payout: 0,
|
||||
netPayout: 0,
|
||||
profit: 0,
|
||||
profitPercent: 0,
|
||||
totalShares: {} as { [outcome: string]: number },
|
||||
hasShares: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function getTopAnswer(
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
) {
|
||||
export function getTopAnswer(contract: FreeResponseContract) {
|
||||
const { answers } = contract
|
||||
const top = maxBy(
|
||||
const top = _.maxBy(
|
||||
answers?.map((answer) => ({
|
||||
answer,
|
||||
prob: getOutcomeProbability(contract, answer.id),
|
||||
|
@ -260,43 +189,3 @@ export function getTopAnswer(
|
|||
)
|
||||
return top?.answer
|
||||
}
|
||||
|
||||
export function getLargestPosition(contract: Contract, userBets: Bet[]) {
|
||||
let yesFloorShares = 0,
|
||||
yesShares = 0,
|
||||
noShares = 0,
|
||||
noFloorShares = 0
|
||||
|
||||
if (userBets.length === 0) {
|
||||
return null
|
||||
}
|
||||
if (contract.outcomeType === 'FREE_RESPONSE') {
|
||||
const answerCounts: { [outcome: string]: number } = {}
|
||||
for (const bet of userBets) {
|
||||
if (bet.outcome) {
|
||||
if (!answerCounts[bet.outcome]) {
|
||||
answerCounts[bet.outcome] = bet.amount
|
||||
} else {
|
||||
answerCounts[bet.outcome] += bet.amount
|
||||
}
|
||||
}
|
||||
}
|
||||
const majorityAnswer =
|
||||
maxBy(Object.keys(answerCounts), (outcome) => answerCounts[outcome]) ?? ''
|
||||
return {
|
||||
prob: undefined,
|
||||
shares: answerCounts[majorityAnswer] || 0,
|
||||
outcome: majorityAnswer,
|
||||
}
|
||||
}
|
||||
|
||||
const [yesBets, noBets] = partition(userBets, (bet) => bet.outcome === 'YES')
|
||||
yesShares = sumBy(yesBets, (bet) => bet.shares)
|
||||
noShares = sumBy(noBets, (bet) => bet.shares)
|
||||
yesFloorShares = Math.floor(yesShares)
|
||||
noFloorShares = Math.floor(noShares)
|
||||
|
||||
const shares = yesFloorShares || noFloorShares
|
||||
const outcome = yesFloorShares > noFloorShares ? 'YES' : 'NO'
|
||||
return { shares, outcome }
|
||||
}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import { difference } from 'lodash'
|
||||
|
||||
export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default'
|
||||
|
||||
export const CATEGORIES = {
|
||||
politics: 'Politics',
|
||||
technology: 'Technology',
|
||||
science: 'Science',
|
||||
world: 'World',
|
||||
sports: 'Sports',
|
||||
economics: 'Economics',
|
||||
personal: 'Personal',
|
||||
culture: 'Culture',
|
||||
manifold: 'Manifold',
|
||||
covid: 'Covid',
|
||||
crypto: 'Crypto',
|
||||
gaming: 'Gaming',
|
||||
fun: 'Fun',
|
||||
}
|
||||
|
||||
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',
|
||||
'covid',
|
||||
'gaming',
|
||||
'crypto',
|
||||
]
|
||||
|
||||
export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES)
|
||||
|
||||
export const DEFAULT_CATEGORY_GROUPS = DEFAULT_CATEGORIES.map((c) => ({
|
||||
slug: c.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX,
|
||||
name: CATEGORIES[c as category],
|
||||
}))
|
|
@ -1,65 +0,0 @@
|
|||
import { IS_PRIVATE_MANIFOLD } from './envs/constants'
|
||||
|
||||
export type Challenge = {
|
||||
// The link to send: https://manifold.markets/challenges/username/market-slug/{slug}
|
||||
// Also functions as the unique id for the link.
|
||||
slug: string
|
||||
|
||||
// The user that created the challenge.
|
||||
creatorId: string
|
||||
creatorUsername: string
|
||||
creatorName: string
|
||||
creatorAvatarUrl?: string
|
||||
|
||||
// Displayed to people claiming the challenge
|
||||
message: string
|
||||
|
||||
// How much to put up
|
||||
creatorAmount: number
|
||||
|
||||
// YES or NO for now
|
||||
creatorOutcome: string
|
||||
|
||||
// Different than the creator
|
||||
acceptorOutcome: string
|
||||
acceptorAmount: number
|
||||
|
||||
// The probability the challenger thinks
|
||||
creatorOutcomeProb: number
|
||||
|
||||
contractId: string
|
||||
contractSlug: string
|
||||
contractQuestion: string
|
||||
contractCreatorUsername: string
|
||||
|
||||
createdTime: number
|
||||
// If null, the link is valid forever
|
||||
expiresTime: number | null
|
||||
|
||||
// How many times the challenge can be used
|
||||
maxUses: number
|
||||
|
||||
// Used for simpler caching
|
||||
acceptedByUserIds: string[]
|
||||
// Successful redemptions of the link
|
||||
acceptances: Acceptance[]
|
||||
|
||||
// TODO: will have to fill this on resolve contract
|
||||
isResolved: boolean
|
||||
resolutionOutcome?: string
|
||||
}
|
||||
|
||||
export type Acceptance = {
|
||||
// User that accepted the challenge
|
||||
userId: string
|
||||
userUsername: string
|
||||
userName: string
|
||||
userAvatarUrl: string
|
||||
|
||||
// The ID of the successful bet that tracks the money moved
|
||||
betId: string
|
||||
|
||||
createdTime: number
|
||||
}
|
||||
|
||||
export const CHALLENGES_ENABLED = !IS_PRIVATE_MANIFOLD
|
|
@ -1,608 +0,0 @@
|
|||
export interface Charity {
|
||||
id: string
|
||||
slug: string
|
||||
name: string
|
||||
website: string
|
||||
ein?: string
|
||||
photo?: string
|
||||
preview: string
|
||||
description: string
|
||||
tags?: CharityTag[]
|
||||
}
|
||||
|
||||
type CharityTag = 'Featured' // | 'Health' | 'Poverty' | 'X-Risk' | 'Animal Welfare' | 'Policy'
|
||||
|
||||
// Warning: 'name' is currently used as the slug and the txn toId for the charity.
|
||||
export const charities: Charity[] = [
|
||||
{
|
||||
name: '1Day Sooner',
|
||||
website: 'https://www.1daysooner.org/',
|
||||
preview:
|
||||
'Accelerating the development of each vaccine by even a couple of days via COVID-19 human challenge trials could save thousands of lives.',
|
||||
photo: 'https://i.imgur.com/bUDdzUE.png',
|
||||
description: `1Day Sooner is a non-profit that advocates on behalf of COVID-19 challenge trial volunteers.
|
||||
|
||||
After a vaccine candidate is created in a lab, it is developed through a combination of pre-clinical evaluation and three phases of clinical trials that test its safety and efficacy. In traditional Phase III trials, participants receive the vaccine candidate or a placebo/active comparator, and efficacy is judged by comparing the prevalence of infection in the vaccine group and the placebo/comparator group, to test the hypothesis that significantly fewer participants in the vaccine group get infected. In these traditional trials, after receiving the treatment, participants return to their homes and their normal daily lives so as to test the treatment under real world conditions. Since only a small proportion of these participants may encounter the disease, it may take a large number of participants and a good deal of time for these trials to reveal differences between the vaccine and placebo groups.
|
||||
|
||||
In a human challenge trial (HCT), willing participants would receive the vaccine candidate or placebo and, after some time for the vaccine to take effect, be deliberately exposed to live coronavirus. Since exposure to the virus is guaranteed in HCTs, it may be possible to judge a vaccine candidate’s efficacy more quickly and with far fewer participants than a standard Phase III trial. While HCT efficacy results do not traditionally provide sufficient basis for licensure on their own, they could allow us to (1) more quickly weed out disappointing vaccine candidates or (2) promote the development of promising candidates in conjunction with traditional Phase III studies.
|
||||
|
||||
In addition, by gathering detailed data on the process of infection and vaccine protection in a clinical setting, researchers could learn information that proves extremely useful for broader vaccine and therapeutic development efforts. Altogether, there are scenarios in which the speed of HCTs and the richness of the data they provide accelerate the development of an effective and broadly accessible COVID-19 vaccine, with thousands of lives spared (depending on the pandemic’s long-term trajectory).`,
|
||||
tags: ['Featured'] as CharityTag[],
|
||||
},
|
||||
{
|
||||
name: 'QURI',
|
||||
website: 'https://quantifieduncertainty.org/',
|
||||
preview:
|
||||
'The Quantified Uncertainty Research Institute advances forecasting and epistemics to improve the long-term future of humanity.',
|
||||
photo: 'https://i.imgur.com/ZsSXPjH.png',
|
||||
description: `QURI researches systematic practices to specify and estimate the most important parameters for the most important or scalable decisions. Research areas include forecasting, epistemics, evaluations, ontology, and estimation.
|
||||
|
||||
We emphasize technological solutions that can heavily scale in the next 5 to 30 years.
|
||||
|
||||
We believe that humanity’s success in the next few hundred years will lie intensely on its ability to coordinate and make good decisions. If important governmental and philanthropic bodies become significantly more effective, this will make society far more resilient to many kinds of challenges ahead.`,
|
||||
tags: ['Featured'] as CharityTag[],
|
||||
},
|
||||
{
|
||||
name: 'Long-Term Future Fund',
|
||||
website: 'https://funds.effectivealtruism.org/funds/far-future',
|
||||
photo: 'https://i.imgur.com/C2qka9g.png',
|
||||
preview:
|
||||
'The Long-Term Future Fund aims to improve the long-term trajectory of civilization by making grants that address global catastrophic risks.',
|
||||
description: `The Long-Term Future Fund aims to positively influence the long-term trajectory of civilization by making grants that address global catastrophic risks, especially potential risks from advanced artificial intelligence and pandemics. In addition, we seek to promote, implement, and advocate for longtermist ideas, and to otherwise increase the likelihood that future generations will flourish.
|
||||
|
||||
The Fund has a broad remit to make grants that promote, implement and advocate for longtermist ideas. Many of our grants aim to address potential risks from advanced artificial intelligence and to build infrastructure and advocate for longtermist projects. However, we welcome applications related to long-term institutional reform or other global catastrophic risks (e.g., pandemics or nuclear conflict).
|
||||
|
||||
We intend to support:
|
||||
- Projects that directly contribute to reducing existential risks through technical research, policy analysis, advocacy, and/or demonstration projects
|
||||
- Training for researchers or practitioners who work to mitigate existential risks, or help with relevant recruitment efforts, or infrastructure for people working on longtermist projects
|
||||
- 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 couldn’t 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',
|
||||
photo: 'https://i.imgur.com/C2qka9g.png',
|
||||
preview:
|
||||
"The Global Health and Development Fund aims to improve people's lives, typically in the poorest regions of the world where the need for healthcare and economic empowerment is greatest.",
|
||||
description: `The Global Health and Development Fund recommends grants with the aim of improving people's lives, typically in the poorest regions of the world where the need for healthcare and economic empowerment is greatest. This will be achieved primarily by supporting projects that:
|
||||
|
||||
- Directly provide healthcare, or preventive measures that will improve health, well-being, or life expectancy
|
||||
- Directly provide services that raise incomes or otherwise improve economic conditions
|
||||
- Provide assistance to governments in the design and implementation of effective policies
|
||||
|
||||
In addition, the Global Health and Development Fund has a broad remit, and may fund other activities whose ultimate purpose is to serve people living in the poorest regions of the world, for example by raising additional funds (e.g. One for the World) or by exploring novel financing arrangements (e.g. Instiglio).
|
||||
|
||||
The Fund manager recommends grants to GiveWell top charities as a baseline, but will recommend higher-risk grants they believe to be more effective (in expectation) than GiveWell top charities. As such, the fund makes grants with a variety of different risk profiles.`,
|
||||
},
|
||||
{
|
||||
name: 'Animal Welfare Fund',
|
||||
website: 'https://funds.effectivealtruism.org/funds/animal-welfare',
|
||||
photo: 'https://i.imgur.com/C2qka9g.png',
|
||||
preview:
|
||||
'The Animal Welfare Fund aims to effectively improve the well-being of nonhuman animals.',
|
||||
description: `The Animal Welfare Fund aims to effectively improve the well-being of nonhuman animals, by making grants that focus on one or more of the following:
|
||||
|
||||
- Relatively neglected geographic regions or groups of animals
|
||||
- Promising research into animal advocacy or animal well-being
|
||||
- Activities that could make it easier to help animals in the future
|
||||
- Otherwise best-in-class opportunities
|
||||
|
||||
The Fund focuses on projects that primarily address farmed animals, as well as projects that could affect other large populations of nonhuman animals. Some examples of projects that the Fund could support:
|
||||
|
||||
- Supporting farmed animal advocacy in Asia
|
||||
- Researching ways to improve the welfare of farmed fish
|
||||
- Promoting alternative proteins in order to reduce demand for animal products
|
||||
- Advocating against the use of some cruel practice within the industrial agriculture system
|
||||
- Growing the field of welfare biology in order to improve our understanding of different ways to address wild animal suffering`,
|
||||
},
|
||||
{
|
||||
name: 'Effective Altruism Infrastructure Fund',
|
||||
website: 'https://funds.effectivealtruism.org/funds/ea-community',
|
||||
photo: 'https://i.imgur.com/C2qka9g.png',
|
||||
preview:
|
||||
'The Effective Altruism Infrastructure Fund aims to increase the impact of projects that use the principles of effective altruism.',
|
||||
description: `The Effective Altruism Infrastructure Fund (EA Infrastructure Fund) recommends grants that aim to improve the work of projects using principles of effective altruism, by increasing their access to talent, capital, and knowledge.
|
||||
|
||||
The EA Infrastructure Fund has historically attempted to make strategic grants to incubate and grow projects that attempt to use reason and evidence to do as much good as possible. These include meta-charities that fundraise for highly effective charities doing direct work on important problems, research organizations that improve our understanding of how to do good more effectively, and projects that promote principles of effective altruism in contexts like academia.`,
|
||||
},
|
||||
{
|
||||
name: 'Nonlinear',
|
||||
website: 'https://www.nonlinear.org/',
|
||||
photo: 'https://i.imgur.com/Muifc1l.png',
|
||||
preview:
|
||||
'Incubate longtermist nonprofits by connecting founders with ideas, funding, and mentorship.',
|
||||
description: `Problem: There are tens of thousands of people working full time to make AI powerful, but around one hundred working to make AI safe. This needs to change.
|
||||
|
||||
Longtermism is held back by two bottlenecks:
|
||||
1. Lots of funding, but few charities to deploy it.
|
||||
2. Lots of talent, but few charities creating jobs.
|
||||
|
||||
Solution: Longtermism needs more charities to deploy funding and create jobs. Our goal is to 10x the number of talented people working on longtermism by launching dozens of high impact charities.
|
||||
|
||||
This helps solve the bottlenecks because entrepreneurs “unlock” latent EA talent - if one person starts an organization that employs 100 people who weren’t previously working on AI safety, that doubles the number of people working on the problem.
|
||||
|
||||
Our process:
|
||||
1. Research the highest leverage ideas
|
||||
2. Find the right founders
|
||||
3. Connect them with mentors and funding
|
||||
|
||||
We will be announcing more details about our incubation program soon.
|
||||
|
||||
A few of the ideas we’ve incubated so far:
|
||||
- The Nonlinear Library: Listen to top EA content on your podcast player. We use text-to-speech software to create an automatically updating repository of audio content from the EA Forum, Alignment Forum, and LessWrong. You can find it on all major podcast players here.
|
||||
- EA Hiring Agency: Helping EA orgs scalably hire talent.
|
||||
- EA Houses: EA's Airbnb - Connecting EAs who have extra space with EAs who need space here.`,
|
||||
tags: ['Featured'] as CharityTag[],
|
||||
},
|
||||
{
|
||||
name: 'GiveWell Maximum Impact Fund',
|
||||
website: 'https://www.givewell.org/maximum-impact-fund',
|
||||
photo: 'https://i.imgur.com/xikuDMZ.png',
|
||||
preview:
|
||||
'We search for the charities that save or improve lives the most per dollar.',
|
||||
description: `
|
||||
GiveWell is a nonprofit dedicated to finding outstanding giving opportunities and publishing the full details of our analysis to help donors decide where to give.
|
||||
|
||||
We don't focus solely on financials, such as assessing administrative or fundraising costs. Instead, we conduct in-depth research to determine how much good a given program accomplishes (in terms of lives saved, lives improved, etc.) per dollar spent. Rather than rating as many charities as possible, we focus on the few charities that stand out most (by our criteria) in order to find and confidently recommend high-impact giving opportunities (our list of top charities).
|
||||
|
||||
Our top recommendation to GiveWell donors seeking to do the most good possible is to donate to the Maximum Impact Fund. Donations to the Maximum Impact Fund are granted each quarter. We use our latest research to grant the funds to the recommended charity (or charities) where we believe they’ll do the most good.
|
||||
|
||||
We grant funds from the Maximum Impact Fund to the recipient charity (or charities) at the end of each fiscal quarter. Our research team decides which charities have the highest priority funding needs at that time. This decision takes into consideration factors such as:
|
||||
|
||||
- Which funding gaps we expect to be filled and unfilled
|
||||
- Each charity’s plans for additional funding
|
||||
- The cost-effectiveness of each funding gap`,
|
||||
},
|
||||
{
|
||||
name: "Founder's Pledge Climate Change Fund",
|
||||
website: 'https://founderspledge.com/funds/climate-change-fund',
|
||||
photo: 'https://i.imgur.com/9turaJW.png',
|
||||
preview:
|
||||
'The Climate Change Fund aims to sustainably reach net-zero emissions globally, while still allowing growth to free millions from energy poverty.',
|
||||
description: `The Climate Change Fund aims to sustainably reach net-zero emissions globally.
|
||||
|
||||
Current levels of emissions are contributing to millions of deaths annually from air pollution and causing irrevocable damage to our planet. In addition, millions worldwide do not have access to modern energy technology, severely hampering development goals.
|
||||
|
||||
This Fund is committed to finding and funding sustainable solutions to the emissions crisis that still allow growth, freeing millions from the prison of energy poverty.
|
||||
|
||||
The Fund is a philanthropic co-funding vehicle that does not provide investment returns.`,
|
||||
},
|
||||
{
|
||||
name: "Founder's Pledge Patient Philanthropy Fund",
|
||||
website: 'https://founderspledge.com/funds/patient-philanthropy-fund',
|
||||
photo: 'https://i.imgur.com/LLR6CI6.png',
|
||||
preview:
|
||||
'The Patient Philanthropy Project aims to safeguard and benefit the long-term future of humanity',
|
||||
description: `The Patient Philanthropy Project focuses on how we can collectively grow our resources to support the long-term flourishing of humanity. It addresses a crucial gap: as a society, we spend much too little on safeguarding and benefiting future generations. In fact, we spend more money on ice cream each year than we do on preventing our own extinction. However, people in the future - who do not have a voice in their future survival or environment - matter. Lots of them may yet come into existence and we have the ability to positively affect their lives now, if only by making sure we avoid major catastrophes that could destroy our common future.
|
||||
|
||||
Housed within the Project is the Patient Philanthropy Fund, a philanthropic co-funding vehicle which invests to give and ensures capital is at the ready when extraordinary opportunities to safeguard and improve the long-term future arise.
|
||||
|
||||
The Fund’s patient approach means that we aim to identify the point in time when the highest-impact opportunities are available, which may be years, decades, or even centuries ahead.`,
|
||||
},
|
||||
{
|
||||
name: 'ARC',
|
||||
website: 'https://alignment.org/',
|
||||
photo: 'https://i.imgur.com/Hwg8OMP.png',
|
||||
preview: 'Align future machine learning systems with human interests.',
|
||||
description: `ARC is a non-profit research organization whose mission is to align future machine learning systems with human interests. Its current work focuses on developing an alignment strategy that could be adopted in industry today while scaling gracefully to future ML systems. Right now Paul Christiano and Mark Xu are researchers and Kyle Scott handles operations.
|
||||
|
||||
What is “alignment”? ML systems can exhibit goal-directed behavior, but it is difficult to understand or control what they are “trying” to do. Powerful models could cause harm if they were trying to manipulate and deceive humans. The goal of intent alignment is to instead train these models to be helpful and honest.
|
||||
|
||||
Motivation: We believe that modern ML techniques would lead to severe misalignment if scaled up to large enough computers and datasets. Practitioners may be able to adapt before these failures have catastrophic consequences, but we could reduce the risk by adopting scalable methods further in advance.
|
||||
|
||||
What we’re working on: The best way to understand our research priorities and methodology is probably to read our report on Eliciting Latent Knowledge. At a high level, we’re trying to figure out how to train ML systems to answer questions by straightforwardly “translating” their beliefs into natural language rather than by reasoning about what a human wants to hear.
|
||||
|
||||
Methodology: We’re unsatisfied with an algorithm if we can see any plausible story about how it eventually breaks down, which means that we can rule out most algorithms on paper without ever implementing them. The cost of this approach is that it may completely miss strategies that exploit important structure in realistic ML models; the benefit is that you can consider lots of ideas quickly. (More)
|
||||
|
||||
Future plans: We expect to focus on similar theoretical problems in alignment until we either become more pessimistic about tractability or ARC grows enough to branch out into other areas. Over the long term we are likely to work on a combination of theoretical and empirical alignment research, collaborations with industry labs, alignment forecasting, and ML deployment policy.`,
|
||||
},
|
||||
{
|
||||
name: 'Give Directly',
|
||||
website: 'https://www.givedirectly.org/',
|
||||
ein: '27-1661997',
|
||||
photo: 'https://i.imgur.com/lrdxSyd.jpg',
|
||||
preview: 'Send money directly to people living in poverty.',
|
||||
description:
|
||||
'GiveDirectly is a nonprofit that lets donors like you send money directly to the world’s poorest households. We believe people living in poverty deserve the dignity to choose for themselves how best to improve their lives — cash enables that choice. Since 2009, we’ve delivered $500M+ in cash directly into the hands of over 1 million families living in poverty. We currently have operations in Kenya, Rwanda, Liberia, Malawi, Morocco, Mozambique, DRC, Uganda, and the United States.',
|
||||
},
|
||||
{
|
||||
name: 'Hellen Keller International',
|
||||
website: 'https://www.hki.org/',
|
||||
ein: '13-5562162',
|
||||
photo: 'https://i.imgur.com/Dl97Abk.jpg',
|
||||
preview:
|
||||
'We envision a world where no one is deprived of the opportunity to live a healthy life – and reach their true potential.',
|
||||
description:
|
||||
'Right now, 36 million people worldwide — most of them in developing countries — are blind.\n 90 percent of them didn’t have to lose their sight. Helen Keller International is dedicated to combating the causes and consequences of vision loss and making clear vision a reality for those most vulnerable to disease and who lack access to quality eye care.\n Last year alone, we helped provide many tens of millions of people with treatment to prevent diseases of poverty including blinding trachoma and river blindness.\n Surgeons trained by our staff also performed tens of thousands of cataract surgeries in the developing world. And in the United States, we screened the vision of nearly 66,000 students living in some of our country’s poorest neighborhoods and provided free eyeglasses to just over 16,000 of them. ',
|
||||
},
|
||||
{
|
||||
name: 'Against Malaria Foundation',
|
||||
website: 'https://www.againstmalaria.com/',
|
||||
ein: '20-3069841',
|
||||
photo: 'https://i.imgur.com/F3JoZi9.png',
|
||||
preview: 'We help protect people from malaria.',
|
||||
description:
|
||||
'AMF (againstmalaria.com) provides funding for long-lasting insecticide-treated net (LLIN) distributions (for protection against malaria) in developing countries. There is strong evidence that distributing LLINs reduces child mortality and malaria cases. AMF conducts post-distribution surveys of completed distributions to determine whether LLINs have reached their intended destinations and how long they remain in good condition.',
|
||||
},
|
||||
{
|
||||
name: 'Rethink Charity',
|
||||
website: 'https://rethink.charity/',
|
||||
photo: 'https://i.imgur.com/Go7N7As.png',
|
||||
preview:
|
||||
'Providing vital support to high-impact charities and charitable projects.',
|
||||
description: `At Rethink Charity, we’re excited about improving the world by providing vital support to high-impact charities and charitable projects. We equip them with tools to boost their impact, through our projects that empower their donors with tax-efficient giving options and strategically coordinated matching opportunities.
|
||||
What we do:
|
||||
|
||||
- Rethink Charity Forward is a cause-neutral donation routing fund for high-impact charities around the world. Canadians have used RC Forward to donate $10 million to high-impact charities since the project was launched in late 2017.
|
||||
|
||||
- EA Giving Tuesday supports both donors and highly effective nonprofits participating in Facebook’s annual Giving Tuesday match. In addition to setting up systems and processes, the team provides analysis-based recommendations, detailed instructions, and responsive support. The team’s goal is to make it as easy as possible for donors to direct matching dollars to highly effective nonprofits.`,
|
||||
},
|
||||
{
|
||||
name: 'Malaria Consortium',
|
||||
website: 'https://www.malariaconsortium.org/',
|
||||
ein: '98-0627052',
|
||||
photo: 'https://i.imgur.com/LGwy9d8.png ',
|
||||
preview:
|
||||
'We specialise in the prevention, control and treatment of malaria and other communicable diseases.',
|
||||
description:
|
||||
'We are dedicated to ensuring our work is supported by strong evidence and remains grounded in the lessons we learn through implementation. We explore beyond current practice, to try out innovative ways – through research, implementation and policy development – to achieve effective and sustainable disease management and control.',
|
||||
},
|
||||
{
|
||||
name: 'The Center for the Study of Partisanship and Ideology',
|
||||
website: 'https://cspicenter.org/',
|
||||
photo: 'https://i.imgur.com/O88tkOW.png',
|
||||
preview:
|
||||
'Support and fund research on how ideology and government policy contribute to scientific, technological, and social progress.',
|
||||
description: `Over the last few decades, scientific and technological progress have stagnated. Scientists conduct more research than ever before, but groundbreaking innovation is scarce. At the same time, identity politics and political polarization have reached new extremes, and social trends such as family stability and crime are worse than in previous decades and in some cases moving in the wrong direction. What explains these trends, and how can we reverse them?
|
||||
|
||||
Much of the blame lies with the institutions we rely on for administration, innovation, and leadership. Instead of forward-looking governments, we have short-sighted politicians and bloated bureaucracies. Instead of real experts with proven track records, we have so-called ‘experts’ who appeal to the authority of their credentials. Instead of political leaders willing to face facts and make tough tradeoffs, we have politicians who appeal to ignorance and defer responsibility.
|
||||
|
||||
To fix our institutions, we need to rethink them from the ground up. That is why CSPI supports and funds research into the administrative systems, organizational structures, and political ideologies of modern governance. Only by understanding what makes these systems so often dysfunctional can we change them for the better.
|
||||
|
||||
CSPI believes that governments should be accountable to the populace as a whole, not special interest groups. We think experts should have greater say in public policy, but that there should be different standards for what qualifies as “expertise.” We want to end scientific and technological stagnation and usher in a new era of growth and innovation.
|
||||
|
||||
We are interested in funding and supporting research that can speak to these issues in the social sciences through grants and fellowships. CSPI particularly seek outs work that is unlikely to receive support elsewhere. See our home page for more about the kinds of research we are particularly interested in funding.`,
|
||||
},
|
||||
{
|
||||
name: 'Faunalytics',
|
||||
website: 'https://faunalytics.org/',
|
||||
ein: '01-0686889',
|
||||
photo: 'https://i.imgur.com/3JXhuXl.jpg',
|
||||
preview:
|
||||
'Faunalytics conducts research and shares knowledge to help advocates help animals effectively.',
|
||||
description:
|
||||
"Faunalytics' mission is to empower animal advocates with access to research, analysis, strategies, and messages that maximize their effectiveness to reduce animal suffering.\n Animals need you, and you need data. We conduct essential research, maintain an online research library, and directly support advocates and organizations in their work to save lives. The range of data we offer helps our movement understand how people think about and respond to advocacy, providing advocates with the best strategies to inspire change for animals. ",
|
||||
},
|
||||
{
|
||||
name: 'The Humane League',
|
||||
website: 'https://thehumaneleague.org/',
|
||||
ein: '04-3817491',
|
||||
photo: 'https://i.imgur.com/za9Rwon.jpg',
|
||||
preview:
|
||||
'We exist to end the abuse of animals raised for food by influencing the policies of the world’s biggest companies, demanding legislation, and empowering others to take action and leave animals off their plates',
|
||||
description:
|
||||
'The Humane League (THL) currently operates in the U.S., Mexico, the U.K., and Japan, where they work to improve animal welfare standards through grassroots campaigns, movement building, veg*n advocacy, research, and advocacy training, as well as through corporate, media, and community outreach. They work to build the animal advocacy movement internationally through the Open Wing Alliance (OWA), a coalition founded by THL whose mission is to end the use of battery cages globally.',
|
||||
},
|
||||
{
|
||||
name: 'Wild Animal Initiative',
|
||||
website: 'https://www.wildanimalinitiative.org/',
|
||||
ein: '82-2281466',
|
||||
tags: ['Featured'] as CharityTag[],
|
||||
photo: 'https://i.imgur.com/bOVUnDm.png',
|
||||
preview:
|
||||
'Our mission is to understand and improve the lives of wild animals.',
|
||||
description: `Although the natural world is a source of great beauty and happiness, vast numbers of animals routinely face serious challenges such as disease, hunger, or natural disasters. There is no “one-size-fits-all” solution to these threats. However, even as we recognize that improving the welfare of free-ranging wild animals is difficult, we believe that humans have a responsibility to help whenever we can.
|
||||
|
||||
Our staff explores how humans can beneficially coexist with animals through the lens of wild animal welfare.
|
||||
|
||||
We respect wild animals as individuals with their own needs and preferences, rather than seeing them as mere parts of ecosystems. But this approach demands a richer understanding of wild animals’ lives.
|
||||
|
||||
We want to take a proactive approach to managing the welfare benefits, threats, and uncertainties that are inherent to complex natural and urban environments. Yet, to take action safely, we must conduct research to understand the impacts of our actions. The transdisciplinary perspective of wild animal welfare draws upon ethics, ecology, and animal welfare science to gather the knowledge we need, facilitating evidence-based improvements to wild animals’ quality of life.
|
||||
|
||||
Without sufficient public interest or research activity, solutions to the problems wild animals face will go undiscovered.
|
||||
|
||||
Wild Animal Initiative currently focuses on helping scientists, grantors, and decision-makers investigate important and understudied questions about wild animal welfare. Our work catalyzes research and applied projects that will open the door to a clearer picture of wild animals’ needs and how to enhance their well-being. Ultimately, we envision a world in which people actively choose to help wild animals — and have the knowledge they need to do so responsibly.`,
|
||||
},
|
||||
{
|
||||
name: 'FYXX Foundation',
|
||||
website: 'https://www.fyxxfoundation.org/',
|
||||
photo: 'https://i.imgur.com/ROmWO7m.png',
|
||||
preview:
|
||||
'FYXX Foundation: wildlife population management, without killing.',
|
||||
description: `The future of our planet depends on the innovations of today, and the health of our wildlife are the first indication of our successful stewardship, which we believe can be improved by safe population management utilizing fertility control instead of poison and culling.`,
|
||||
},
|
||||
{
|
||||
name: 'New Incentives',
|
||||
website: 'https://www.newincentives.org/',
|
||||
ein: '45-2368993',
|
||||
photo: 'https://i.imgur.com/bYl4tk3.png',
|
||||
preview: 'Cash incentives to boost vaccination rates and save lives.',
|
||||
description:
|
||||
'New Incentives (newincentives.org) runs a conditional cash transfer (CCT) program in North West Nigeria which seeks to increase uptake of routine immunizations through cash transfers, raising public awareness of the benefits of vaccination and reducing the frequency of vaccine stockouts.',
|
||||
},
|
||||
{
|
||||
name: 'SCI foundation',
|
||||
website: 'https://schistosomiasiscontrolinitiative.org/',
|
||||
ein: '',
|
||||
photo: 'https://i.imgur.com/sWD8zM5.png',
|
||||
preview:
|
||||
'SCI works with governments in sub-Saharan Africa to create or scale up programs that treat schistosomiasis and soil-transmitted helminthiasis ("deworming").',
|
||||
description:
|
||||
'We’re a non-profit initiative supporting governments in sub-Saharan African countries. We support them to develop sustainable, cost-effective programmes against parasitic worm infections such as schistosomiasis and intestinal worms. Since our foundation in 2002, we’ve contributed to the delivery of over 200 million treatments against these diseases. The programmes are highly effective; parasitic worm infections can be reduced by up to 60% after just one round of treatment.',
|
||||
},
|
||||
{
|
||||
name: 'Wikimedia Foundation',
|
||||
website: 'https://wikimediafoundation.org/',
|
||||
ein: '20-0049703',
|
||||
photo: 'https://i.imgur.com/klEzUbR.png',
|
||||
preview: 'We help everyone share in the sum of all knowledge.',
|
||||
description:
|
||||
'We are the people who keep knowledge free. There is an amazing community of people around the world that makes great projects like Wikipedia. We help them do that work. We take care of the technical infrastructure, the legal challenges, and the growing pains.',
|
||||
},
|
||||
{
|
||||
name: 'Rainforest Trust',
|
||||
website: 'https://www.rainforesttrust.org/',
|
||||
ein: '13-3500609',
|
||||
photo: 'https://i.imgur.com/6MzS530.png',
|
||||
preview:
|
||||
'Rainforest Trust saves endangered wildlife and protects our planet by creating rainforest reserves through partnerships, community engagement and donor support.',
|
||||
description:
|
||||
'Our unique, cost-effective conservation model for protecting endangered species has been implemented successfully for over 30 years. Thanks to the generosity of our donors, the expertise of our partners and the participation of local communities across the tropics, our reserves are exemplary models of international conservation.',
|
||||
},
|
||||
{
|
||||
name: 'The Nature Conservancy',
|
||||
website: 'https://www.nature.org/en-us/',
|
||||
ein: '53-0242652',
|
||||
photo: 'https://i.imgur.com/vjxkoGo.jpg',
|
||||
preview: 'A Future Where People and Nature Thrive',
|
||||
description:
|
||||
'The Nature Conservancy is a global environmental nonprofit working to create a world where people and nature can thrive. Founded in the U.S. through grassroots action in 1951, The Nature Conservancy has grown to become one of the most effective and wide-reaching environmental organizations in the world. Thanks to more than a million members and the dedicated efforts of our diverse staff and over 400 scientists, we impact conservation in 76 countries and territories: 37 by direct conservation impact and 39 through partners.',
|
||||
},
|
||||
{
|
||||
name: 'Doctors Without Borders',
|
||||
website: 'https://www.doctorswithoutborders.org/',
|
||||
ein: '13-3433452',
|
||||
photo: 'https://i.imgur.com/xqhH9FE.png',
|
||||
preview:
|
||||
'We provide independent, impartial medical humanitarian assistance to the people who need it most.',
|
||||
description:
|
||||
'Doctors Without Borders/Médecins Sans Frontières (MSF) cares for people affected by conflict, disease outbreaks, natural and human-made disasters, and exclusion from health care in more than 70 countries.',
|
||||
},
|
||||
{
|
||||
name: 'World Wildlife Fund',
|
||||
website: 'https://www.worldwildlife.org/',
|
||||
ein: '52-1693387',
|
||||
photo: 'https://i.imgur.com/hDADuqW.png',
|
||||
preview:
|
||||
'WWF works to sustain the natural world for the benefit of people and wildlife, collaborating with partners from local to global levels in nearly 100 countries.',
|
||||
description:
|
||||
'As the world’s leading conservation organization, WWF works in nearly 100 countries to tackle the most pressing issues at the intersection of nature, people, and climate. We collaborate with local communities to conserve the natural resources we all depend on and build a future in which people and nature thrive. Together with partners at all levels, we transform markets and policies toward sustainability, tackle the threats driving the climate crisis, and protect and restore wildlife and their habitats.',
|
||||
},
|
||||
{
|
||||
name: 'UNICEF USA',
|
||||
website: 'https://www.unicefusa.org/',
|
||||
photo: 'https://i.imgur.com/9cxuvZi.png',
|
||||
ein: '13-1760110',
|
||||
preview:
|
||||
"UNICEF USA helps save and protect the world's most vulnerable children.",
|
||||
description:
|
||||
'Over eight decades, the United Nations Children’s Fund (UNICEF) has built an unprecedented global support system for the world’s children. UNICEF relentlessly works day in and day out to deliver the essentials that give every child an equitable chance in life: health care and immunizations, safe water and sanitation, nutrition, education, emergency relief and more. UNICEF USA advances the global mission of UNICEF by rallying the American public to support the world’s most vulnerable children. Together, we have helped save more children’s lives than any other humanitarian organization.',
|
||||
},
|
||||
{
|
||||
name: 'Vitamin Angels',
|
||||
website: 'https://www.vitaminangels.org/',
|
||||
ein: '77-0485881',
|
||||
photo: 'https://i.imgur.com/Mf35IOu.jpg',
|
||||
preview:
|
||||
'By improving access to vital nutrition, everyone gets an equal chance to grow, thrive, and prosper.',
|
||||
description:
|
||||
'Our team of program experts collaborates with thousands of local organizations and national governments around the world, focusing efforts on reaching communities who are underserved. Vitamin Angels’ program partners are a local presence in these communities. As trusted organizations already hard at work, they connect millions of pregnant women and young children with our evidence-based nutrition interventions in addition to the health services they already provide.',
|
||||
},
|
||||
{
|
||||
name: 'Free Software Foundation',
|
||||
website: 'https://www.fsf.org/',
|
||||
ein: '04-2888848',
|
||||
photo: 'https://i.imgur.com/z87sFDE.png',
|
||||
preview:
|
||||
'The Free Software Foundation (FSF) is a nonprofit with a worldwide mission to promote computer user freedom.',
|
||||
description:
|
||||
'As our society grows more dependent on computers, the software we run is of critical importance to securing the future of a free society. Free software is about having control over the technology we use in our homes, schools and businesses, where computers work for our individual and communal benefit, not for proprietary software companies or governments who might seek to restrict and monitor us. The Free Software Foundation exclusively uses free software to perform its work.The Free Software Foundation is working to secure freedom for computerusers by promoting the development and use of free (as in freedom) software and documentation—particularly the GNU operating system—and by campaigning against threats to computer user freedom like Digital Restrictions Management (DRM) and software patents.',
|
||||
},
|
||||
{
|
||||
name: 'Direct Relief',
|
||||
website: 'https://www.directrelief.org/',
|
||||
ein: '95-1831116',
|
||||
photo: 'https://i.imgur.com/QS7kHAU.png',
|
||||
preview:
|
||||
'Direct Relief is a humanitarian aid organization, active in all 50 states and more than 80 countries, with a mission to improve the health and lives of people affected by poverty or emergencies – without regard to politics, religion, or ability to pay.',
|
||||
description:
|
||||
'Nongovernmental, nonsectarian, and not-for-profit, Direct Relief relies entirely on private contributions to advance its mission and perform a wide range of functions.\n Included among them are identifying key local providers of health services; working to identify the unmet needs of people in the low-resource areas; mobilizing essential medicines, supplies, and equipment that are requested and appropriate for the circumstances; and managing the many details inherent in storing, transporting, and distributing such resources to organizations in the most efficient manner possible.',
|
||||
},
|
||||
{
|
||||
name: 'World Resources Institute',
|
||||
website: 'https://www.wri.org/',
|
||||
ein: '52-1257057',
|
||||
photo: 'https://i.imgur.com/Bi6MgYI.png',
|
||||
preview:
|
||||
'WRI is a global nonprofit organization that works with leaders in government, business and civil society to research, design, and carry out practical solutions that simultaneously improve people’s lives and ensure nature can thrive.',
|
||||
description:
|
||||
"Since its founding in 1982, WRI has been guided by its mission and core values which are integrated into all that we do. Our mission: To move human society to live in ways that protect Earth’s environment and its capacity to provide for the needs and aspirations of current and future generations. WRI relies on the generosity of our donors to drive outcomes that help the world to be a fairer, healthier and more sustainable place for people and the planet. We publish our financials annually to highlight our continued fiscal accountability. That's why WRI consistently receives top ratings from charity evaluators for our strong financial stewardship and commitment to transparency and accountability.",
|
||||
},
|
||||
{
|
||||
name: 'ProPublica',
|
||||
website: 'https://www.propublica.org/',
|
||||
ein: '14-2007220',
|
||||
photo: 'https://i.imgur.com/R5Vt3Pb.png',
|
||||
preview:
|
||||
'The mission: to expose abuses of power and betrayals of the public trust by government, business, and other institutions, using the moral force of investigative journalism to spur reform through the sustained spotlighting of wrongdoing.',
|
||||
description:
|
||||
'ProPublica is an independent, nonprofit newsroom that produces investigative journalism with moral force. We dig deep into important issues, shining a light on abuses of power and betrayals of public trust — and we stick with those issues as long as it takes to hold power to account. With a team of more than 100 dedicated journalists, ProPublica covers a range of topics including government and politics, business, criminal justice, the environment, education, health care, immigration, and technology. We focus on stories with the potential to spur real-world impact. Among other positive changes, our reporting has contributed to the passage of new laws; reversals of harmful policies and practices; and accountability for leaders at local, state and national levels.',
|
||||
},
|
||||
{
|
||||
name: 'Dana-Farber Cancer Institute',
|
||||
website: 'https://www.dana-farber.org/',
|
||||
ein: '04-2263040',
|
||||
photo: 'https://i.imgur.com/SQNn97p.png',
|
||||
preview:
|
||||
"For over 70 years, we've led the world by making life-changing breakthroughs in cancer research and patient care, providing the most advanced treatments available.",
|
||||
description:
|
||||
"Since its founding in 1947, Dana-Farber Cancer Institute in Boston, Massachusetts has been committed to providing adults and children with cancer with the best treatment available today while developing tomorrow's cures through cutting-edge research. Today, the Institute employs more than 5,000 staff, faculty, and clinicians supporting more than 640,000 annual outpatient visits, more than 1,000 hospital discharges per year, and has over 1,100 open clinical trials. Dana-Farber is internationally renowned for its equal commitment to cutting edge research and provision of excellent patient care. The deep expertise in these two areas uniquely positions Dana-Farber to develop, test, and gain FDA approval for new cancer therapies in its laboratories and clinical settings. Dana-Farber researchers have contributed to the development of 35 of 75 cancer drugs recently approved by the FDA for use in cancer patients.",
|
||||
},
|
||||
{
|
||||
name: 'Save The Children',
|
||||
website: 'https://www.savethechildren.org/',
|
||||
ein: '06-0726487',
|
||||
photo: 'https://i.imgur.com/GngYPBI.png',
|
||||
preview:
|
||||
'Through the decades, Save the Children has continued to work to save children’s lives, and that’s still what we do today.',
|
||||
description:
|
||||
"Our pioneering programs address children's unique needs, giving them a healthy start in life, the opportunity to learn and protection from harm. In the United States and around the world, our work creates lasting change for children, their families and communities – ultimately, transforming the future we all share.\nThis work is only made possible by the ongoing generosity of our donors, whose valuable support is used in the most cost-effective ways. It's important to note that all our work intersects – helping a boy or girl go to school also protects them from dangers such as child trafficking and early marriage. Keeping children healthy from disease or malnutrition means their parents are more likely to avoid costly treatment and be better able to provide for their family.\nWe don’t go into communities, carry out a project and then move on. We consult with children, their families, community leaders and local councils to understand all the issues or barriers, and then we develop programs that address these. We build trust so that our programs are successful and bring about real change.",
|
||||
},
|
||||
{
|
||||
name: 'World Central Kitchen Incorporated',
|
||||
website: 'https://wck.org/',
|
||||
ein: '27-3521132',
|
||||
photo: 'https://i.imgur.com/te93MaY.png',
|
||||
preview:
|
||||
'WCK is first to the frontlines, providing meals in response to humanitarian, climate, and community crises. We build resilient food systems with locally led solutions.',
|
||||
description:
|
||||
"WCK responds to natural disasters, man-made crises, and humanitarian emergencies around the world. We're a team of food first responders, mobilizing with the urgency of now to get meals to the people who need them most. Deploying our model of quick action, leveraging local resources, and adapting in real time, we know that a nourishing meal in a time of crisis is so much more than a plate of food—it's hope, it's dignity, and it's a sign that someone cares.",
|
||||
},
|
||||
{
|
||||
name: 'The Johns Hopkins Center for Health Security',
|
||||
website: 'https://www.centerforhealthsecurity.org/',
|
||||
ein: '',
|
||||
photo: 'https://i.imgur.com/gKZE2Xs.png',
|
||||
preview:
|
||||
'Our mission: to protect people’s health from epidemics and disasters and ensure that communities are resilient to major challenges.',
|
||||
description:
|
||||
'The Center for Health Security undertakes a series of projects, collaborations, and initiatives to push forward progress on global health security, emerging infectious diseases and epidemics, medical and public health preparedness and response, deliberate biological threats, and opportunities and risks in the life sciences. We:\n- Conduct research and analysis on major domestic and international health security issues.\n- Engage with researchers, the policymaking community, and the private sector to make progress in the field.\n- Convene expert working groups, congressional seminars, scientific meetings, conferences, and tabletop exercises to stimulate new thinking and provoke action.\n- Educate a rising generation of scholars, practitioners, and policymakers.',
|
||||
},
|
||||
{
|
||||
name: 'ALLFED',
|
||||
website: 'https://allfed.info/',
|
||||
photo: 'https://i.imgur.com/p235vwF.jpg',
|
||||
ein: '27-6601178',
|
||||
preview: 'Feeding everyone no matter what.',
|
||||
description:
|
||||
'The mission of the Alliance to Feed the Earth in Disasters is to help create resilience to global food shocks. We seek to identify various resilient food solutions and to help governments implement these solutions, to increase the chances that people have enough to eat in the event of a global catastrophe. We focus on events that could deplete food supplies or access to 5% of the global population or more.Our ultimate goal is to feed everyone, no matter what. An important aspect of this goal is that we need to establish equitable solutions so that all people can access the nutrition they need, regardless of wealth or location.ALLFED is inspired by effective altruism, using reason and evidence to identify how to do the most good. Our solutions are backed by science and research, and we also identify the most cost-effective solutions, to be able to provide more nutrition in catastrophes.',
|
||||
},
|
||||
{
|
||||
name: 'The Trevor Project',
|
||||
website: 'https://www.thetrevorproject.org/',
|
||||
photo: 'https://i.imgur.com/QN4mVNn.jpeg',
|
||||
preview:
|
||||
'The Trevor Project is the world’s 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 we’re 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.
|
||||
|
||||
Our Vision
|
||||
A world where all LGBTQ young people see a bright future for themselves.
|
||||
|
||||
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.`,
|
||||
},
|
||||
{
|
||||
name: 'The Center for Election Science',
|
||||
website: 'https://electionscience.org/',
|
||||
photo: 'https://i.imgur.com/WvdHHZa.png',
|
||||
preview:
|
||||
'The Center for Election Science is a nonpartisan nonprofit dedicated to empowering voters with voting methods that strengthen democracy. We believe you deserve a vote that empowers you to impact the world you live in.',
|
||||
description: `Founded in 2011, The Center for Election Science is a national, nonpartisan nonprofit focused on voting reform.
|
||||
|
||||
Our Mission — To empower people with voting methods that strengthen democracy.
|
||||
|
||||
Our Vision — A world where democracies thrive because voters’ voices are heard.
|
||||
|
||||
With an emphasis on approval voting, we bring better elections to people across the country through both advocacy and research.
|
||||
|
||||
The movement for a better way to vote is rapidly gaining momentum as voters grow tired of election results that don’t represent the will of the people. In 2018, we worked with locals in Fargo, ND to help them become the first city in the U.S. to adopt approval voting. And in 2020, we helped grassroots activists empower the 300k people of St. Louis, MO with stronger democracy through approval voting.`,
|
||||
},
|
||||
{
|
||||
name: 'Founders Pledge Global Health and Development Fund',
|
||||
website: 'https://founderspledge.com/funds/global-health-and-development',
|
||||
photo: 'https://i.imgur.com/EXbxH7T.png',
|
||||
preview:
|
||||
'Tackling the vast global inequalities in health, wealth and opportunity',
|
||||
description: `Nearly half the world lives on less than $2.50 a day, yet giving by the world’s richest often overlooks the world’s poorest and most vulnerable. Despite the average American household being richer than 90% of the rest of the world, only 6% of US charitable giving goes to charities which work internationally.
|
||||
|
||||
This Fund is focused on helping those who need it most, wherever that help can make the biggest difference. By building a mixed portfolio of direct and indirect interventions, such as policy work, we aim to:
|
||||
|
||||
Improve the lives of the world's most vulnerable people.
|
||||
Reduce the number of easily preventable deaths worldwide.
|
||||
Work towards sustainable, systemic change.`,
|
||||
},
|
||||
{
|
||||
name: 'YIMBY Law',
|
||||
website: 'https://www.yimbylaw.org/',
|
||||
photo: 'https://i.imgur.com/zlzp21Z.png',
|
||||
preview:
|
||||
'YIMBY Law works to make housing in California more accessible and affordable, by enforcing state housing laws.',
|
||||
description: `
|
||||
YIMBY Law works to make housing in California more accessible and affordable. Our method is to enforce state housing laws, and some examples are outlined below. We send letters to cities considering zoning or general plan compliant housing developments informing them of their duties under state law, and sue them when they do not comply.
|
||||
|
||||
If you would like to support our work, you can do so by getting involved or by donating.`,
|
||||
},
|
||||
{
|
||||
name: 'CaRLA',
|
||||
website: 'https://carlaef.org/',
|
||||
photo: 'https://i.imgur.com/IsNVTOY.png',
|
||||
preview:
|
||||
'The California Renters Legal Advocacy and Education Fund’s core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.',
|
||||
description: `
|
||||
The California Renters Legal Advocacy and Education Fund’s core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.
|
||||
|
||||
CaRLA uses legal advocacy and education to ensure all cities comply with their own zoning and state housing laws and do their part to help solve the state’s housing shortage.
|
||||
|
||||
In addition to housing impact litigation, we provide free legal aid, education and workshops, counseling and advocacy to advocates, homeowners, small developers, and city and state government officials.`,
|
||||
},
|
||||
{
|
||||
name: 'Mriya',
|
||||
website: 'https://mriya-ua.org/',
|
||||
photo:
|
||||
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2Fdefault%2Fci2h3hStFM.47?alt=media&token=0d2cdc3d-e4d8-4f5e-8f23-4a586b6ff637',
|
||||
preview: 'Donate supplies to soldiers in Ukraine',
|
||||
description:
|
||||
'Donate supplies to soldiers in Ukraine, including tourniquets and plate carriers.',
|
||||
},
|
||||
].map((charity) => {
|
||||
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
|
||||
return {
|
||||
...charity,
|
||||
id: slug,
|
||||
slug,
|
||||
}
|
||||
})
|
|
@ -1,56 +1,16 @@
|
|||
import type { JSONContent } from '@tiptap/core'
|
||||
|
||||
export type AnyCommentType = OnContract | OnGroup | OnPost
|
||||
|
||||
// Currently, comments are created after the bet, not atomically with the bet.
|
||||
// They're uniquely identified by the pair contractId/betId.
|
||||
export type Comment<T extends AnyCommentType = AnyCommentType> = {
|
||||
export type Comment = {
|
||||
id: string
|
||||
replyToCommentId?: string
|
||||
contractId: string
|
||||
betId?: string
|
||||
userId: string
|
||||
|
||||
/** @deprecated - content now stored as JSON in content*/
|
||||
text?: string
|
||||
content: JSONContent
|
||||
text: string
|
||||
createdTime: number
|
||||
|
||||
// Denormalized, for rendering comments
|
||||
userName: string
|
||||
userUsername: string
|
||||
userAvatarUrl?: string
|
||||
bountiesAwarded?: number
|
||||
} & T
|
||||
|
||||
export type OnContract = {
|
||||
commentType: 'contract'
|
||||
contractId: string
|
||||
answerOutcome?: string
|
||||
betId?: string
|
||||
|
||||
// denormalized from contract
|
||||
contractSlug: string
|
||||
contractQuestion: string
|
||||
|
||||
// denormalized from bet
|
||||
betAmount?: number
|
||||
betOutcome?: string
|
||||
|
||||
// denormalized based on betting history
|
||||
commenterPositionProb?: number // binary only
|
||||
commenterPositionShares?: number
|
||||
commenterPositionOutcome?: string
|
||||
}
|
||||
|
||||
export type OnGroup = {
|
||||
commentType: 'group'
|
||||
groupId: string
|
||||
}
|
||||
|
||||
export type OnPost = {
|
||||
commentType: 'post'
|
||||
postId: string
|
||||
}
|
||||
|
||||
export type ContractComment = Comment<OnContract>
|
||||
export type GroupComment = Comment<OnGroup>
|
||||
export type PostComment = Comment<OnPost>
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
import { Challenge } from './challenge'
|
||||
import { BinaryContract, Contract } from './contract'
|
||||
import { getFormattedMappedValue } from './pseudo-numeric'
|
||||
import { getProbability } from './calculate'
|
||||
import { richTextToString } from './util/parse'
|
||||
import { getCpmmProbability } from './calculate-cpmm'
|
||||
import { getDpmProbability } from './calculate-dpm'
|
||||
import { formatMoney, formatPercent } from './util/format'
|
||||
|
||||
export function contractMetrics(contract: Contract) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const dayjs = require('dayjs')
|
||||
const { createdTime, resolutionTime, isResolved } = contract
|
||||
|
||||
const createdDate = dayjs(createdTime).format('MMM D')
|
||||
|
||||
const resolvedDate = isResolved
|
||||
? dayjs(resolutionTime).format('MMM D')
|
||||
: undefined
|
||||
|
||||
const volumeLabel = `${formatMoney(contract.volume)} bet`
|
||||
|
||||
return { volumeLabel, createdDate, resolvedDate }
|
||||
}
|
||||
|
||||
// String version of the above, to send to the OpenGraph image generator
|
||||
export function contractTextDetails(contract: Contract) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const dayjs = require('dayjs')
|
||||
const { closeTime, groupLinks } = contract
|
||||
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
|
||||
|
||||
const groupHashtags = groupLinks?.map((g) => `#${g.name.replace(/ /g, '')}`)
|
||||
|
||||
return (
|
||||
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
|
||||
(closeTime
|
||||
? ` • ${closeTime > Date.now() ? 'Closes' : 'Closed'} ${dayjs(
|
||||
closeTime
|
||||
).format('MMM D, h:mma')}`
|
||||
: '') +
|
||||
` • ${volumeLabel}` +
|
||||
(groupHashtags ? ` • ${groupHashtags.join(' ')}` : '')
|
||||
)
|
||||
}
|
||||
|
||||
export function getBinaryProb(contract: BinaryContract) {
|
||||
const { pool, resolutionProbability, mechanism } = contract
|
||||
|
||||
return (
|
||||
resolutionProbability ??
|
||||
(mechanism === 'cpmm-1'
|
||||
? getCpmmProbability(pool, contract.p)
|
||||
: getDpmProbability(contract.totalShares))
|
||||
)
|
||||
}
|
||||
|
||||
export const getOpenGraphProps = (contract: Contract) => {
|
||||
const {
|
||||
resolution,
|
||||
question,
|
||||
creatorName,
|
||||
creatorUsername,
|
||||
outcomeType,
|
||||
creatorAvatarUrl,
|
||||
description: desc,
|
||||
} = contract
|
||||
const probPercent =
|
||||
outcomeType === 'BINARY'
|
||||
? formatPercent(getBinaryProb(contract))
|
||||
: undefined
|
||||
|
||||
const numericValue =
|
||||
outcomeType === 'PSEUDO_NUMERIC'
|
||||
? getFormattedMappedValue(contract)(getProbability(contract))
|
||||
: undefined
|
||||
|
||||
const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc)
|
||||
|
||||
const description = resolution
|
||||
? `Resolved ${resolution}. ${stringDesc}`
|
||||
: probPercent
|
||||
? `${probPercent} chance. ${stringDesc}`
|
||||
: stringDesc
|
||||
|
||||
return {
|
||||
question,
|
||||
probability: probPercent,
|
||||
metadata: contractTextDetails(contract),
|
||||
creatorName,
|
||||
creatorUsername,
|
||||
creatorAvatarUrl,
|
||||
description,
|
||||
numericValue,
|
||||
resolution,
|
||||
}
|
||||
}
|
||||
|
||||
export type OgCardProps = {
|
||||
question: string
|
||||
probability?: string
|
||||
metadata: string
|
||||
creatorName: string
|
||||
creatorUsername: string
|
||||
creatorAvatarUrl?: string
|
||||
numericValue?: string
|
||||
resolution?: string
|
||||
}
|
||||
|
||||
export function buildCardUrl(props: OgCardProps, challenge?: Challenge) {
|
||||
const {
|
||||
creatorAmount,
|
||||
acceptances,
|
||||
acceptorAmount,
|
||||
creatorOutcome,
|
||||
acceptorOutcome,
|
||||
} = challenge || {}
|
||||
const {
|
||||
probability,
|
||||
numericValue,
|
||||
resolution,
|
||||
creatorAvatarUrl,
|
||||
question,
|
||||
metadata,
|
||||
creatorUsername,
|
||||
creatorName,
|
||||
} = props
|
||||
const { userName, userAvatarUrl } = acceptances?.[0] ?? {}
|
||||
|
||||
const probabilityParam =
|
||||
probability === undefined
|
||||
? ''
|
||||
: `&probability=${encodeURIComponent(probability ?? '')}`
|
||||
|
||||
const numericValueParam =
|
||||
numericValue === undefined
|
||||
? ''
|
||||
: `&numericValue=${encodeURIComponent(numericValue ?? '')}`
|
||||
|
||||
const creatorAvatarUrlParam =
|
||||
creatorAvatarUrl === undefined
|
||||
? ''
|
||||
: `&creatorAvatarUrl=${encodeURIComponent(creatorAvatarUrl ?? '')}`
|
||||
|
||||
const challengeUrlParams = challenge
|
||||
? `&creatorAmount=${creatorAmount}&creatorOutcome=${creatorOutcome}` +
|
||||
`&challengerAmount=${acceptorAmount}&challengerOutcome=${acceptorOutcome}` +
|
||||
`&acceptedName=${userName ?? ''}&acceptedAvatarUrl=${userAvatarUrl ?? ''}`
|
||||
: ''
|
||||
|
||||
const resolutionUrlParam = resolution
|
||||
? `&resolution=${encodeURIComponent(resolution)}`
|
||||
: ''
|
||||
|
||||
// URL encode each of the props, then add them as query params
|
||||
return (
|
||||
`https://manifold-og-image.vercel.app/m.png` +
|
||||
`?question=${encodeURIComponent(question)}` +
|
||||
probabilityParam +
|
||||
numericValueParam +
|
||||
`&metadata=${encodeURIComponent(metadata)}` +
|
||||
`&creatorName=${encodeURIComponent(creatorName)}` +
|
||||
creatorAvatarUrlParam +
|
||||
`&creatorUsername=${encodeURIComponent(creatorUsername)}` +
|
||||
challengeUrlParams +
|
||||
resolutionUrlParam
|
||||
)
|
||||
}
|
|
@ -1,83 +1,45 @@
|
|||
import { Answer } from './answer'
|
||||
import { Fees } from './fees'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
import { GroupLink } from 'common/group'
|
||||
|
||||
export type AnyMechanism = DPM | CPMM
|
||||
export type AnyOutcomeType =
|
||||
| Binary
|
||||
| MultipleChoice
|
||||
| PseudoNumeric
|
||||
| FreeResponse
|
||||
| Numeric
|
||||
|
||||
export type AnyContractType =
|
||||
| (CPMM & Binary)
|
||||
| (CPMM & PseudoNumeric)
|
||||
| (DPM & Binary)
|
||||
| (DPM & FreeResponse)
|
||||
| (DPM & Numeric)
|
||||
| (DPM & MultipleChoice)
|
||||
|
||||
export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||
export type FullContract<
|
||||
M extends DPM | CPMM,
|
||||
T extends Binary | Multi | FreeResponse
|
||||
> = {
|
||||
id: string
|
||||
slug: string // auto-generated; must be unique
|
||||
|
||||
creatorId: string
|
||||
creatorName: string
|
||||
creatorUsername: string
|
||||
creatorAvatarUrl?: string
|
||||
creatorAvatarUrl?: string // Start requiring after 2022-03-01
|
||||
|
||||
question: string
|
||||
description: string | JSONContent // More info about what the contract is about
|
||||
description: string // More info about what the contract is about
|
||||
tags: string[]
|
||||
lowercaseTags: string[]
|
||||
visibility: visibility
|
||||
visibility: 'public' | 'unlisted'
|
||||
|
||||
createdTime: number // Milliseconds since epoch
|
||||
lastUpdatedTime?: number // Updated on new bet or comment
|
||||
lastBetTime?: number
|
||||
lastCommentTime?: number
|
||||
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?: string
|
||||
resolutionProbability?: number
|
||||
|
||||
closeEmailsSent?: number
|
||||
|
||||
volume: number
|
||||
volume24Hours: number
|
||||
volume7Days: number
|
||||
elasticity: number
|
||||
|
||||
collectedFees: Fees
|
||||
} & M &
|
||||
T
|
||||
|
||||
groupSlugs?: string[]
|
||||
groupLinks?: GroupLink[]
|
||||
uniqueBettorIds?: string[]
|
||||
uniqueBettorCount?: number
|
||||
popularityScore?: number
|
||||
dailyScore?: number
|
||||
followerCount?: number
|
||||
featuredOnHomeRank?: number
|
||||
likedByUserIds?: string[]
|
||||
likedByUserCount?: number
|
||||
flaggedByUsernames?: string[]
|
||||
openCommentBounties?: number
|
||||
unlistedById?: string
|
||||
} & T
|
||||
|
||||
export type BinaryContract = Contract & Binary
|
||||
export type PseudoNumericContract = Contract & PseudoNumeric
|
||||
export type NumericContract = Contract & Numeric
|
||||
export type FreeResponseContract = Contract & FreeResponse
|
||||
export type MultipleChoiceContract = Contract & MultipleChoice
|
||||
export type DPMContract = Contract & DPM
|
||||
export type CPMMContract = Contract & CPMM
|
||||
export type DPMBinaryContract = BinaryContract & DPM
|
||||
export type CPMMBinaryContract = BinaryContract & CPMM
|
||||
export type Contract = FullContract<DPM | CPMM, Binary | Multi | FreeResponse>
|
||||
export type BinaryContract = FullContract<DPM | CPMM, Binary>
|
||||
export type FreeResponseContract = FullContract<DPM | CPMM, FreeResponse>
|
||||
|
||||
export type DPM = {
|
||||
mechanism: 'dpm-2'
|
||||
|
@ -92,33 +54,22 @@ export type CPMM = {
|
|||
mechanism: 'cpmm-1'
|
||||
pool: { [outcome: string]: number }
|
||||
p: number // probability constant in y^p * n^(1-p) = k
|
||||
totalLiquidity: number // for historical reasons, this the total subsidy amount added in M$
|
||||
subsidyPool: number // current value of subsidy pool in M$
|
||||
prob: number
|
||||
probChanges: {
|
||||
day: number
|
||||
week: number
|
||||
month: number
|
||||
}
|
||||
totalLiquidity: number // in M$
|
||||
}
|
||||
|
||||
export type FixedPayouts = CPMM
|
||||
|
||||
export type Binary = {
|
||||
outcomeType: 'BINARY'
|
||||
initialProbability: number
|
||||
resolutionProbability?: number // Used for BINARY markets resolved to MKT
|
||||
resolution?: resolution
|
||||
resolution?: 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
||||
}
|
||||
|
||||
export type PseudoNumeric = {
|
||||
outcomeType: 'PSEUDO_NUMERIC'
|
||||
min: number
|
||||
max: number
|
||||
isLogScale: boolean
|
||||
resolutionValue?: number
|
||||
|
||||
// same as binary market; map everything to probability
|
||||
initialProbability: number
|
||||
resolutionProbability?: number
|
||||
export type Multi = {
|
||||
outcomeType: 'MULTI'
|
||||
multiOutcomes: string[] // Used for outcomeType 'MULTI'.
|
||||
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||
}
|
||||
|
||||
export type FreeResponse = {
|
||||
|
@ -128,38 +79,8 @@ export type FreeResponse = {
|
|||
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||
}
|
||||
|
||||
export type MultipleChoice = {
|
||||
outcomeType: 'MULTIPLE_CHOICE'
|
||||
answers: Answer[]
|
||||
resolution?: string | 'MKT' | 'CANCEL'
|
||||
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||
}
|
||||
export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE'
|
||||
|
||||
export type Numeric = {
|
||||
outcomeType: 'NUMERIC'
|
||||
bucketCount: number
|
||||
min: number
|
||||
max: number
|
||||
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||
resolutionValue?: number
|
||||
}
|
||||
|
||||
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',
|
||||
'MULTIPLE_CHOICE',
|
||||
'FREE_RESPONSE',
|
||||
'PSEUDO_NUMERIC',
|
||||
'NUMERIC',
|
||||
] as const
|
||||
|
||||
export const MAX_QUESTION_LENGTH = 240
|
||||
export const MAX_DESCRIPTION_LENGTH = 16000
|
||||
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
|
||||
|
||||
export type visibility = 'public' | 'unlisted'
|
||||
export const VISIBILITIES = ['public', 'unlisted'] as const
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import { ENV_CONFIG } from './envs/constants'
|
||||
|
||||
const econ = ENV_CONFIG.economy
|
||||
|
||||
export const FIXED_ANTE = econ?.FIXED_ANTE ?? 100
|
||||
|
||||
export const STARTING_BALANCE = econ?.STARTING_BALANCE ?? 1000
|
||||
// for sus users, i.e. multiple sign ups for same person
|
||||
export const SUS_STARTING_BALANCE = econ?.SUS_STARTING_BALANCE ?? 10
|
||||
export const REFERRAL_AMOUNT = econ?.REFERRAL_AMOUNT ?? 250
|
||||
|
||||
export const UNIQUE_BETTOR_BONUS_AMOUNT = econ?.UNIQUE_BETTOR_BONUS_AMOUNT ?? 10
|
||||
export const BETTING_STREAK_BONUS_AMOUNT =
|
||||
econ?.BETTING_STREAK_BONUS_AMOUNT ?? 5
|
||||
export const BETTING_STREAK_BONUS_MAX = econ?.BETTING_STREAK_BONUS_MAX ?? 25
|
||||
export const BETTING_STREAK_RESET_HOUR = econ?.BETTING_STREAK_RESET_HOUR ?? 7
|
||||
export const FREE_MARKETS_PER_USER_MAX = econ?.FREE_MARKETS_PER_USER_MAX ?? 5
|
||||
export const COMMENT_BOUNTY_AMOUNT = econ?.COMMENT_BOUNTY_AMOUNT ?? 250
|
||||
|
||||
export const UNIQUE_BETTOR_LIQUIDITY = 20
|
|
@ -1,17 +1,16 @@
|
|||
import { escapeRegExp } from 'lodash'
|
||||
import { DEV_CONFIG } from './dev'
|
||||
import { EnvConfig, PROD_CONFIG } from './prod'
|
||||
import { THEOREMONE_CONFIG } from './theoremone'
|
||||
|
||||
export const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
|
||||
const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
|
||||
|
||||
const CONFIGS: { [env: string]: EnvConfig } = {
|
||||
const CONFIGS = {
|
||||
PROD: PROD_CONFIG,
|
||||
DEV: DEV_CONFIG,
|
||||
THEOREMONE: THEOREMONE_CONFIG,
|
||||
}
|
||||
|
||||
export const ENV_CONFIG = CONFIGS[ENV]
|
||||
// @ts-ignore
|
||||
export const ENV_CONFIG: EnvConfig = CONFIGS[ENV]
|
||||
|
||||
export function isWhitelisted(email?: string) {
|
||||
if (!ENV_CONFIG.whitelistEmail) {
|
||||
|
@ -21,38 +20,11 @@ export function isWhitelisted(email?: string) {
|
|||
}
|
||||
|
||||
// TODO: Before open sourcing, we should turn these into env vars
|
||||
export function isAdmin(email?: string) {
|
||||
if (!email) {
|
||||
return false
|
||||
}
|
||||
export function isAdmin(email: string) {
|
||||
return ENV_CONFIG.adminEmails.includes(email)
|
||||
}
|
||||
|
||||
export function isManifoldId(userId: string) {
|
||||
return userId === 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2'
|
||||
}
|
||||
|
||||
export const DOMAIN = ENV_CONFIG.domain
|
||||
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
|
||||
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
|
||||
export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE'
|
||||
|
||||
export const AUTH_COOKIE_NAME = `FBUSER_${PROJECT_ID.toUpperCase().replace(
|
||||
/-/g,
|
||||
'_'
|
||||
)}`
|
||||
|
||||
// Manifold's domain or any subdomains thereof
|
||||
export const CORS_ORIGIN_MANIFOLD = new RegExp(
|
||||
'^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$'
|
||||
)
|
||||
// Vercel deployments, used for testing.
|
||||
export const CORS_ORIGIN_VERCEL = new RegExp(
|
||||
'^https?://[a-zA-Z0-9\\-]+' + escapeRegExp('mantic.vercel.app') + '$'
|
||||
)
|
||||
// Any localhost server on any port
|
||||
export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/
|
||||
|
||||
export function firestoreConsolePath(contractId: string) {
|
||||
return `https://console.firebase.google.com/project/${PROJECT_ID}/firestore/data/~2Fcontracts~2F${contractId}`
|
||||
}
|
||||
|
|
|
@ -2,20 +2,13 @@ import { EnvConfig, PROD_CONFIG } from './prod'
|
|||
|
||||
export const DEV_CONFIG: EnvConfig = {
|
||||
...PROD_CONFIG,
|
||||
domain: 'dev.manifold.markets',
|
||||
firebaseConfig: {
|
||||
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
||||
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
||||
projectId: 'dev-mantic-markets',
|
||||
region: 'us-central1',
|
||||
storageBucket: 'dev-mantic-markets.appspot.com',
|
||||
messagingSenderId: '134303100058',
|
||||
appId: '1:134303100058:web:27f9ea8b83347251f80323',
|
||||
measurementId: 'G-YJC9E37P37',
|
||||
},
|
||||
cloudRunId: 'w3txbmd3ba',
|
||||
cloudRunRegion: 'uc',
|
||||
amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3',
|
||||
twitchBotEndpoint: 'https://dev-twitch-bot.manifold.markets',
|
||||
sprigEnvironmentId: 'Tu7kRZPm7daP',
|
||||
}
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
export type EnvConfig = {
|
||||
domain: string
|
||||
firebaseConfig: FirebaseConfig
|
||||
amplitudeApiKey?: string
|
||||
twitchBotEndpoint?: string
|
||||
sprigEnvironmentId?: 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[]
|
||||
|
@ -17,38 +9,15 @@ export type EnvConfig = {
|
|||
|
||||
// Branding
|
||||
moneyMoniker: string // e.g. 'M$'
|
||||
bettor?: string // e.g. 'bettor' or 'predictor'
|
||||
presentBet?: string // e.g. 'bet' or 'predict'
|
||||
pastBet?: string // e.g. 'bet' or 'prediction'
|
||||
faviconPath?: string // Should be a file in /public
|
||||
navbarLogoPath?: string
|
||||
newQuestionPlaceholders: string[]
|
||||
|
||||
economy?: Economy
|
||||
}
|
||||
|
||||
export type Economy = {
|
||||
FIXED_ANTE?: number
|
||||
|
||||
STARTING_BALANCE?: number
|
||||
SUS_STARTING_BALANCE?: number
|
||||
|
||||
REFERRAL_AMOUNT?: number
|
||||
|
||||
UNIQUE_BETTOR_BONUS_AMOUNT?: number
|
||||
|
||||
BETTING_STREAK_BONUS_AMOUNT?: number
|
||||
BETTING_STREAK_BONUS_MAX?: number
|
||||
BETTING_STREAK_RESET_HOUR?: number
|
||||
FREE_MARKETS_PER_USER_MAX?: number
|
||||
COMMENT_BOUNTY_AMOUNT?: number
|
||||
}
|
||||
|
||||
type FirebaseConfig = {
|
||||
apiKey: string
|
||||
authDomain: string
|
||||
projectId: string
|
||||
region?: string
|
||||
storageBucket: string
|
||||
messagingSenderId: string
|
||||
appId: string
|
||||
|
@ -57,39 +26,24 @@ type FirebaseConfig = {
|
|||
|
||||
export const PROD_CONFIG: EnvConfig = {
|
||||
domain: 'manifold.markets',
|
||||
amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15',
|
||||
sprigEnvironmentId: 'sQcrq9TDqkib',
|
||||
|
||||
firebaseConfig: {
|
||||
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
||||
authDomain: 'mantic-markets.firebaseapp.com',
|
||||
projectId: 'mantic-markets',
|
||||
region: 'us-central1',
|
||||
storageBucket: 'mantic-markets.appspot.com',
|
||||
messagingSenderId: '128925704902',
|
||||
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
||||
measurementId: 'G-SSFK1Q138D',
|
||||
},
|
||||
twitchBotEndpoint: 'https://twitch-bot.manifold.markets',
|
||||
cloudRunId: 'nggbo3neva',
|
||||
cloudRunRegion: 'uc',
|
||||
adminEmails: [
|
||||
'akrolsmir@gmail.com', // Austin
|
||||
'jahooma@gmail.com', // James
|
||||
'taowell@gmail.com', // Stephen
|
||||
'abc.sinclair@gmail.com', // Sinclair
|
||||
'manticmarkets@gmail.com', // Manifold
|
||||
'iansphilips@gmail.com', // Ian
|
||||
'd4vidchee@gmail.com', // D4vid
|
||||
'federicoruizcassarino@gmail.com', // Fede
|
||||
'ingawei@gmail.com', //Inga
|
||||
],
|
||||
visibility: 'PUBLIC',
|
||||
|
||||
moneyMoniker: 'M$',
|
||||
bettor: 'trader',
|
||||
pastBet: 'trade',
|
||||
presentBet: 'trade',
|
||||
navbarLogoPath: '',
|
||||
faviconPath: '/favicon.ico',
|
||||
newQuestionPlaceholders: [
|
||||
|
|
|
@ -6,14 +6,11 @@ export const THEOREMONE_CONFIG: EnvConfig = {
|
|||
apiKey: 'AIzaSyBSXL6Ys7InNHnCKSy-_E_luhh4Fkj4Z6M',
|
||||
authDomain: 'theoremone-manifold.firebaseapp.com',
|
||||
projectId: 'theoremone-manifold',
|
||||
region: 'us-central1',
|
||||
storageBucket: 'theoremone-manifold.appspot.com',
|
||||
messagingSenderId: '698012149198',
|
||||
appId: '1:698012149198:web:b342af75662831aa84b79f',
|
||||
measurementId: 'G-Y3EZ1WNT6E',
|
||||
},
|
||||
cloudRunId: 'nggbo3neva', // TODO: fill in real ID for T1
|
||||
cloudRunRegion: 'uc',
|
||||
adminEmails: [...PROD_CONFIG.adminEmails, 'david.glidden@theoremone.co'],
|
||||
whitelistEmail: '@theoremone.co',
|
||||
moneyMoniker: 'T$',
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import { Bet } from './bet'
|
||||
import { Comment } from './comment'
|
||||
import { Contract } from './contract'
|
||||
|
||||
export type feed = {
|
||||
contract: Contract
|
||||
recentBets: Bet[]
|
||||
recentComments: Comment[]
|
||||
}[]
|
|
@ -1,11 +1,9 @@
|
|||
export const FLAT_TRADE_FEE = 0.1 // M$0.1
|
||||
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
|
||||
export const LIQUIDITY_FEE = 0
|
||||
|
||||
export const DPM_PLATFORM_FEE = 0.0
|
||||
export const DPM_CREATOR_FEE = 0.0
|
||||
export const DPM_PLATFORM_FEE = 0.01
|
||||
export const DPM_CREATOR_FEE = 0.04
|
||||
export const DPM_FEES = DPM_PLATFORM_FEE + DPM_CREATOR_FEE
|
||||
|
||||
export type Fees = {
|
||||
|
|
23
common/fold.ts
Normal file
23
common/fold.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
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
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
export type Follow = {
|
||||
userId: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export type ContractFollow = {
|
||||
id: string // user id
|
||||
createdTime: number
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export type GlobalConfig = {
|
||||
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
export type Group = {
|
||||
id: string
|
||||
slug: string
|
||||
name: string
|
||||
about: string
|
||||
creatorId: string // User id
|
||||
createdTime: number
|
||||
mostRecentActivityTime: number
|
||||
anyoneCanJoin: boolean
|
||||
totalContracts: number
|
||||
totalMembers: number
|
||||
aboutPostId?: string
|
||||
postIds: string[]
|
||||
chatDisabled?: boolean
|
||||
mostRecentContractAddedTime?: number
|
||||
cachedLeaderboard?: {
|
||||
topTraders: {
|
||||
userId: string
|
||||
score: number
|
||||
}[]
|
||||
topCreators: {
|
||||
userId: string
|
||||
score: number
|
||||
}[]
|
||||
}
|
||||
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
|
||||
}
|
||||
|
||||
export const MAX_GROUP_NAME_LENGTH = 75
|
||||
export const MAX_ABOUT_LENGTH = 140
|
||||
export const MAX_ID_LENGTH = 60
|
||||
export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome']
|
||||
export const GROUP_CHAT_SLUG = 'chat'
|
||||
|
||||
export type GroupLink = {
|
||||
slug: string
|
||||
name: string
|
||||
groupId: string
|
||||
createdTime: number
|
||||
userId?: string
|
||||
}
|
||||
export type GroupContractDoc = { contractId: string; createdTime: number }
|
|
@ -1,9 +0,0 @@
|
|||
export type Like = {
|
||||
id: string // will be id of the object liked, i.e. contract.id
|
||||
userId: string
|
||||
type: 'contract' | 'post'
|
||||
createdTime: number
|
||||
tipTxnId?: string // only holds most recent tip txn id
|
||||
}
|
||||
export const LIKE_TIP_AMOUNT = 10
|
||||
export const TIP_UNDO_DURATION = 2000
|
138
common/loans.ts
138
common/loans.ts
|
@ -1,138 +0,0 @@
|
|||
import { Dictionary, groupBy, sumBy, minBy } from 'lodash'
|
||||
import { Bet } from './bet'
|
||||
import { getContractBetMetrics } from './calculate'
|
||||
import {
|
||||
Contract,
|
||||
CPMMContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
} from './contract'
|
||||
import { PortfolioMetrics, User } from './user'
|
||||
import { filterDefined } from './util/array'
|
||||
|
||||
const LOAN_DAILY_RATE = 0.02
|
||||
|
||||
const calculateNewLoan = (investedValue: number, loanTotal: number) => {
|
||||
const netValue = investedValue - loanTotal
|
||||
return netValue * LOAN_DAILY_RATE
|
||||
}
|
||||
|
||||
export const getLoanUpdates = (
|
||||
users: User[],
|
||||
contractsById: { [contractId: string]: Contract },
|
||||
portfolioByUser: { [userId: string]: PortfolioMetrics | undefined },
|
||||
betsByUser: { [userId: string]: Bet[] }
|
||||
) => {
|
||||
const eligibleUsers = filterDefined(
|
||||
users.map((user) =>
|
||||
isUserEligibleForLoan(portfolioByUser[user.id]) ? user : undefined
|
||||
)
|
||||
)
|
||||
|
||||
const betUpdates = eligibleUsers
|
||||
.map((user) => {
|
||||
const updates = calculateLoanBetUpdates(
|
||||
betsByUser[user.id] ?? [],
|
||||
contractsById
|
||||
).betUpdates
|
||||
return updates.map((update) => ({ ...update, user }))
|
||||
})
|
||||
.flat()
|
||||
|
||||
const updatesByUser = groupBy(betUpdates, (update) => update.userId)
|
||||
const userPayouts = Object.values(updatesByUser).map((updates) => {
|
||||
return {
|
||||
user: updates[0].user,
|
||||
payout: sumBy(updates, (update) => update.newLoan),
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
betUpdates,
|
||||
userPayouts,
|
||||
}
|
||||
}
|
||||
|
||||
const isUserEligibleForLoan = (portfolio: PortfolioMetrics | undefined) => {
|
||||
if (!portfolio) return true
|
||||
|
||||
const { balance, investmentValue } = portfolio
|
||||
return balance + investmentValue > 0
|
||||
}
|
||||
|
||||
const calculateLoanBetUpdates = (
|
||||
bets: Bet[],
|
||||
contractsById: Dictionary<Contract>
|
||||
) => {
|
||||
const betsByContract = groupBy(bets, (bet) => bet.contractId)
|
||||
const contracts = filterDefined(
|
||||
Object.keys(betsByContract).map((contractId) => contractsById[contractId])
|
||||
).filter((c) => !c.isResolved)
|
||||
|
||||
const betUpdates = filterDefined(
|
||||
contracts
|
||||
.map((c) => {
|
||||
if (c.mechanism === 'cpmm-1') {
|
||||
return getBinaryContractLoanUpdate(c, betsByContract[c.id])
|
||||
} else if (
|
||||
c.outcomeType === 'FREE_RESPONSE' ||
|
||||
c.outcomeType === 'MULTIPLE_CHOICE'
|
||||
)
|
||||
return getFreeResponseContractLoanUpdate(c, betsByContract[c.id])
|
||||
else {
|
||||
// Unsupported contract / mechanism for loans.
|
||||
return []
|
||||
}
|
||||
})
|
||||
.flat()
|
||||
)
|
||||
|
||||
const totalNewLoan = sumBy(betUpdates, (loanUpdate) => loanUpdate.loanTotal)
|
||||
|
||||
return {
|
||||
totalNewLoan,
|
||||
betUpdates,
|
||||
}
|
||||
}
|
||||
|
||||
const getBinaryContractLoanUpdate = (contract: CPMMContract, bets: Bet[]) => {
|
||||
const { invested } = getContractBetMetrics(contract, bets)
|
||||
const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
|
||||
const oldestBet = minBy(bets, (bet) => bet.createdTime)
|
||||
|
||||
const newLoan = calculateNewLoan(invested, loanAmount)
|
||||
if (!isFinite(newLoan) || newLoan <= 0 || !oldestBet) return undefined
|
||||
|
||||
const loanTotal = (oldestBet.loanAmount ?? 0) + newLoan
|
||||
|
||||
return {
|
||||
userId: oldestBet.userId,
|
||||
contractId: contract.id,
|
||||
betId: oldestBet.id,
|
||||
newLoan,
|
||||
loanTotal,
|
||||
}
|
||||
}
|
||||
|
||||
const getFreeResponseContractLoanUpdate = (
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
bets: Bet[]
|
||||
) => {
|
||||
const openBets = bets.filter((bet) => !bet.isSold && !bet.sale)
|
||||
|
||||
return openBets.map((bet) => {
|
||||
const loanAmount = bet.loanAmount ?? 0
|
||||
const newLoan = calculateNewLoan(bet.amount, loanAmount)
|
||||
const loanTotal = loanAmount + newLoan
|
||||
|
||||
if (!isFinite(newLoan) || newLoan <= 0) return undefined
|
||||
|
||||
return {
|
||||
userId: bet.userId,
|
||||
contractId: contract.id,
|
||||
betId: bet.id,
|
||||
newLoan,
|
||||
loanTotal,
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,303 +1,70 @@
|
|||
import { sortBy, sum, sumBy } from 'lodash'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
import { Bet, fill, LimitBet, NumericBet } from './bet'
|
||||
import { Bet, MAX_LOAN_PER_CONTRACT } from './bet'
|
||||
import {
|
||||
calculateDpmShares,
|
||||
getDpmProbability,
|
||||
getDpmOutcomeProbability,
|
||||
getNumericBets,
|
||||
calculateNumericDpmShares,
|
||||
} from './calculate-dpm'
|
||||
import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm'
|
||||
import {
|
||||
calculateCpmmAmountToProb,
|
||||
calculateCpmmPurchase,
|
||||
CpmmState,
|
||||
getCpmmProbability,
|
||||
} from './calculate-cpmm'
|
||||
import {
|
||||
CPMMBinaryContract,
|
||||
DPMBinaryContract,
|
||||
DPMContract,
|
||||
NumericContract,
|
||||
PseudoNumericContract,
|
||||
Binary,
|
||||
CPMM,
|
||||
DPM,
|
||||
FreeResponse,
|
||||
FullContract,
|
||||
Multi,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
import { noFees } from './fees'
|
||||
import { addObjects, removeUndefinedProps } from './util/object'
|
||||
import { NUMERIC_FIXED_VAR } from './numeric-constants'
|
||||
import {
|
||||
floatingEqual,
|
||||
floatingGreaterEqual,
|
||||
floatingLesserEqual,
|
||||
} from './util/math'
|
||||
|
||||
export type CandidateBet<T extends Bet = Bet> = Omit<
|
||||
T,
|
||||
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
|
||||
>
|
||||
export type BetInfo = {
|
||||
newBet: CandidateBet
|
||||
newPool?: { [outcome: string]: number }
|
||||
newTotalShares?: { [outcome: string]: number }
|
||||
newTotalBets?: { [outcome: string]: number }
|
||||
newTotalLiquidity?: number
|
||||
newP?: number
|
||||
}
|
||||
|
||||
const computeFill = (
|
||||
export const getNewBinaryCpmmBetInfo = (
|
||||
user: User,
|
||||
outcome: 'YES' | 'NO',
|
||||
amount: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
limitProb: number | undefined,
|
||||
cpmmState: CpmmState,
|
||||
matchedBet: LimitBet | undefined
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
loanAmount: number,
|
||||
newBetId: string
|
||||
) => {
|
||||
const prob = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
|
||||
if (
|
||||
limitProb !== undefined &&
|
||||
(outcome === 'YES'
|
||||
? floatingGreaterEqual(prob, limitProb) &&
|
||||
(matchedBet?.limitProb ?? 1) > limitProb
|
||||
: floatingLesserEqual(prob, limitProb) &&
|
||||
(matchedBet?.limitProb ?? 0) < limitProb)
|
||||
) {
|
||||
// No fill.
|
||||
return undefined
|
||||
}
|
||||
|
||||
const timestamp = Date.now()
|
||||
|
||||
if (
|
||||
!matchedBet ||
|
||||
(outcome === 'YES'
|
||||
? !floatingGreaterEqual(prob, matchedBet.limitProb)
|
||||
: !floatingLesserEqual(prob, matchedBet.limitProb))
|
||||
) {
|
||||
// Fill from pool.
|
||||
const limit = !matchedBet
|
||||
? limitProb
|
||||
: outcome === 'YES'
|
||||
? Math.min(matchedBet.limitProb, limitProb ?? 1)
|
||||
: Math.max(matchedBet.limitProb, limitProb ?? 0)
|
||||
|
||||
const buyAmount =
|
||||
limit === undefined
|
||||
? amount
|
||||
: Math.min(amount, calculateCpmmAmountToProb(cpmmState, limit, outcome))
|
||||
|
||||
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
|
||||
cpmmState,
|
||||
buyAmount,
|
||||
outcome
|
||||
)
|
||||
const newState = { pool: newPool, p: newP }
|
||||
|
||||
return {
|
||||
maker: {
|
||||
matchedBetId: null,
|
||||
shares,
|
||||
amount: buyAmount,
|
||||
state: newState,
|
||||
fees,
|
||||
timestamp,
|
||||
},
|
||||
taker: {
|
||||
matchedBetId: null,
|
||||
shares,
|
||||
amount: buyAmount,
|
||||
timestamp,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Fill from matchedBet.
|
||||
const matchRemaining = matchedBet.orderAmount - matchedBet.amount
|
||||
const shares = Math.min(
|
||||
amount /
|
||||
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
|
||||
matchRemaining /
|
||||
(outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb)
|
||||
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
|
||||
contract,
|
||||
amount,
|
||||
outcome
|
||||
)
|
||||
|
||||
const maker = {
|
||||
bet: matchedBet,
|
||||
matchedBetId: 'taker',
|
||||
amount:
|
||||
shares *
|
||||
(outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb),
|
||||
shares,
|
||||
timestamp,
|
||||
}
|
||||
const taker = {
|
||||
matchedBetId: matchedBet.id,
|
||||
amount:
|
||||
shares *
|
||||
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
|
||||
shares,
|
||||
timestamp,
|
||||
}
|
||||
return { maker, taker }
|
||||
}
|
||||
const newBalance = user.balance - (amount - loanAmount)
|
||||
|
||||
export const computeFills = (
|
||||
outcome: 'YES' | 'NO',
|
||||
betAmount: number,
|
||||
state: CpmmState,
|
||||
limitProb: number | undefined,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
) => {
|
||||
if (isNaN(betAmount)) {
|
||||
throw new Error('Invalid bet amount: ${betAmount}')
|
||||
}
|
||||
if (isNaN(limitProb ?? 0)) {
|
||||
throw new Error('Invalid limitProb: ${limitProb}')
|
||||
}
|
||||
const { pool, p, totalLiquidity } = contract
|
||||
const probBefore = getCpmmProbability(pool, p)
|
||||
const probAfter = getCpmmProbability(newPool, newP)
|
||||
|
||||
const sortedBets = sortBy(
|
||||
unfilledBets.filter((bet) => bet.outcome !== outcome),
|
||||
(bet) => (outcome === 'YES' ? bet.limitProb : -bet.limitProb),
|
||||
(bet) => bet.createdTime
|
||||
)
|
||||
|
||||
const takers: fill[] = []
|
||||
const makers: {
|
||||
bet: LimitBet
|
||||
amount: number
|
||||
shares: number
|
||||
timestamp: number
|
||||
}[] = []
|
||||
const ordersToCancel: LimitBet[] = []
|
||||
|
||||
let amount = betAmount
|
||||
let cpmmState = { pool: state.pool, p: state.p }
|
||||
let totalFees = noFees
|
||||
const currentBalanceByUserId = { ...balanceByUserId }
|
||||
|
||||
let i = 0
|
||||
while (true) {
|
||||
const matchedBet: LimitBet | undefined = sortedBets[i]
|
||||
const fill = computeFill(amount, outcome, limitProb, cpmmState, matchedBet)
|
||||
if (!fill) break
|
||||
|
||||
const { taker, maker } = fill
|
||||
|
||||
if (maker.matchedBetId === null) {
|
||||
// Matched against pool.
|
||||
cpmmState = maker.state
|
||||
totalFees = addObjects(totalFees, maker.fees)
|
||||
takers.push(taker)
|
||||
} else {
|
||||
// Matched against bet.
|
||||
i++
|
||||
const { userId } = maker.bet
|
||||
const makerBalance = currentBalanceByUserId[userId]
|
||||
|
||||
if (floatingGreaterEqual(makerBalance, maker.amount)) {
|
||||
currentBalanceByUserId[userId] = makerBalance - maker.amount
|
||||
} else {
|
||||
// Insufficient balance. Cancel maker bet.
|
||||
ordersToCancel.push(maker.bet)
|
||||
continue
|
||||
}
|
||||
|
||||
takers.push(taker)
|
||||
makers.push(maker)
|
||||
}
|
||||
|
||||
amount -= taker.amount
|
||||
|
||||
if (floatingEqual(amount, 0)) break
|
||||
}
|
||||
|
||||
return { takers, makers, totalFees, cpmmState, ordersToCancel }
|
||||
}
|
||||
|
||||
export const getBinaryCpmmBetInfo = (
|
||||
outcome: 'YES' | 'NO',
|
||||
betAmount: number,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
limitProb: number | undefined,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
) => {
|
||||
const { pool, p } = contract
|
||||
const { takers, makers, cpmmState, totalFees, ordersToCancel } = computeFills(
|
||||
outcome,
|
||||
betAmount,
|
||||
{ pool, p },
|
||||
limitProb,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
const probBefore = getCpmmProbability(contract.pool, contract.p)
|
||||
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
|
||||
const takerAmount = sumBy(takers, 'amount')
|
||||
const takerShares = sumBy(takers, 'shares')
|
||||
const isFilled = floatingEqual(betAmount, takerAmount)
|
||||
|
||||
const newBet: CandidateBet = removeUndefinedProps({
|
||||
orderAmount: betAmount,
|
||||
amount: takerAmount,
|
||||
shares: takerShares,
|
||||
limitProb,
|
||||
isFilled,
|
||||
isCancelled: false,
|
||||
fills: takers,
|
||||
const newBet: Bet = {
|
||||
id: newBetId,
|
||||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
shares,
|
||||
outcome,
|
||||
fees,
|
||||
loanAmount,
|
||||
probBefore,
|
||||
probAfter,
|
||||
loanAmount: 0,
|
||||
createdTime: Date.now(),
|
||||
fees: totalFees,
|
||||
})
|
||||
|
||||
const { liquidityFee } = totalFees
|
||||
const newTotalLiquidity = (contract.totalLiquidity ?? 0) + liquidityFee
|
||||
|
||||
return {
|
||||
newBet,
|
||||
newPool: cpmmState.pool,
|
||||
newP: cpmmState.p,
|
||||
newTotalLiquidity,
|
||||
makers,
|
||||
ordersToCancel,
|
||||
}
|
||||
}
|
||||
|
||||
export const getBinaryBetStats = (
|
||||
outcome: 'YES' | 'NO',
|
||||
betAmount: number,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
limitProb: number,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
) => {
|
||||
const { newBet } = getBinaryCpmmBetInfo(
|
||||
outcome,
|
||||
betAmount ?? 0,
|
||||
contract,
|
||||
limitProb,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
const remainingMatched =
|
||||
((newBet.orderAmount ?? 0) - newBet.amount) /
|
||||
(outcome === 'YES' ? limitProb : 1 - limitProb)
|
||||
const currentPayout = newBet.shares + remainingMatched
|
||||
const { liquidityFee } = fees
|
||||
const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee
|
||||
|
||||
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
||||
|
||||
const totalFees = sum(Object.values(newBet.fees))
|
||||
|
||||
return { currentPayout, currentReturn, totalFees, newBet }
|
||||
return { newBet, newPool, newP, newBalance, newTotalLiquidity, fees }
|
||||
}
|
||||
|
||||
export const getNewBinaryDpmBetInfo = (
|
||||
user: User,
|
||||
outcome: 'YES' | 'NO',
|
||||
amount: number,
|
||||
contract: DPMBinaryContract
|
||||
contract: FullContract<DPM, Binary>,
|
||||
loanAmount: number,
|
||||
newBetId: string
|
||||
) => {
|
||||
const { YES: yesPool, NO: noPool } = contract.pool
|
||||
|
||||
|
@ -325,10 +92,12 @@ export const getNewBinaryDpmBetInfo = (
|
|||
const probBefore = getDpmProbability(contract.totalShares)
|
||||
const probAfter = getDpmProbability(newTotalShares)
|
||||
|
||||
const newBet: CandidateBet = {
|
||||
const newBet: Bet = {
|
||||
id: newBetId,
|
||||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
loanAmount: 0,
|
||||
loanAmount,
|
||||
shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
|
@ -337,13 +106,18 @@ export const getNewBinaryDpmBetInfo = (
|
|||
fees: noFees,
|
||||
}
|
||||
|
||||
return { newBet, newPool, newTotalShares, newTotalBets }
|
||||
const newBalance = user.balance - (amount - loanAmount)
|
||||
|
||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||
}
|
||||
|
||||
export const getNewMultiBetInfo = (
|
||||
user: User,
|
||||
outcome: string,
|
||||
amount: number,
|
||||
contract: DPMContract
|
||||
contract: FullContract<DPM, Multi | FreeResponse>,
|
||||
loanAmount: number,
|
||||
newBetId: string
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
|
||||
|
@ -361,10 +135,12 @@ export const getNewMultiBetInfo = (
|
|||
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
|
||||
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
|
||||
|
||||
const newBet: CandidateBet = {
|
||||
const newBet: Bet = {
|
||||
id: newBetId,
|
||||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
loanAmount: 0,
|
||||
loanAmount,
|
||||
shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
|
@ -373,48 +149,17 @@ export const getNewMultiBetInfo = (
|
|||
fees: noFees,
|
||||
}
|
||||
|
||||
return { newBet, newPool, newTotalShares, newTotalBets }
|
||||
const newBalance = user.balance - (amount - loanAmount)
|
||||
|
||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||
}
|
||||
|
||||
export const getNumericBetsInfo = (
|
||||
value: number,
|
||||
outcome: string,
|
||||
amount: number,
|
||||
contract: NumericContract
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
|
||||
const bets = getNumericBets(contract, outcome, amount, NUMERIC_FIXED_VAR)
|
||||
|
||||
const allBetAmounts = Object.fromEntries(bets)
|
||||
const newTotalBets = addObjects(totalBets, allBetAmounts)
|
||||
const newPool = addObjects(pool, allBetAmounts)
|
||||
|
||||
const { shares, totalShares: newTotalShares } = calculateNumericDpmShares(
|
||||
contract.totalShares,
|
||||
bets
|
||||
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
|
||||
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||
const prevLoanAmount = _.sumBy(openBets, (bet) => bet.loanAmount ?? 0)
|
||||
const loanAmount = Math.min(
|
||||
newBetAmount,
|
||||
MAX_LOAN_PER_CONTRACT - prevLoanAmount
|
||||
)
|
||||
|
||||
const allOutcomeShares = Object.fromEntries(
|
||||
bets.map(([outcome], i) => [outcome, shares[i]])
|
||||
)
|
||||
|
||||
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
|
||||
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
|
||||
|
||||
const newBet: CandidateBet<NumericBet> = {
|
||||
contractId: contract.id,
|
||||
value,
|
||||
amount,
|
||||
allBetAmounts,
|
||||
shares: shares.find((s, i) => bets[i][0] === outcome) ?? 0,
|
||||
allOutcomeShares,
|
||||
outcome,
|
||||
probBefore,
|
||||
probAfter,
|
||||
createdTime: Date.now(),
|
||||
fees: noFees,
|
||||
}
|
||||
|
||||
return { newBet, newPool, newTotalShares, newTotalBets }
|
||||
return loanAmount
|
||||
}
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
import { range } from 'lodash'
|
||||
import { PHANTOM_ANTE } from './antes'
|
||||
import {
|
||||
Binary,
|
||||
Contract,
|
||||
CPMM,
|
||||
DPM,
|
||||
FreeResponse,
|
||||
MultipleChoice,
|
||||
Numeric,
|
||||
outcomeType,
|
||||
PseudoNumeric,
|
||||
visibility,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
import { parseTags } from './util/parse'
|
||||
import { removeUndefinedProps } from './util/object'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
import { calcDpmInitialPool } from './calculate-dpm'
|
||||
|
||||
export function getNewContract(
|
||||
id: string,
|
||||
|
@ -21,33 +18,24 @@ export function getNewContract(
|
|||
creator: User,
|
||||
question: string,
|
||||
outcomeType: outcomeType,
|
||||
description: JSONContent,
|
||||
description: string,
|
||||
initialProb: number,
|
||||
ante: number,
|
||||
closeTime: number,
|
||||
extraTags: string[],
|
||||
|
||||
// used for numeric markets
|
||||
bucketCount: number,
|
||||
min: number,
|
||||
max: number,
|
||||
isLogScale: boolean,
|
||||
|
||||
// for multiple choice
|
||||
answers: string[],
|
||||
visibility: visibility
|
||||
extraTags: string[]
|
||||
) {
|
||||
const tags = parseTags(
|
||||
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
|
||||
)
|
||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||
|
||||
const propsByOutcomeType =
|
||||
outcomeType === 'BINARY'
|
||||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
||||
: outcomeType === 'PSEUDO_NUMERIC'
|
||||
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
|
||||
: outcomeType === 'NUMERIC'
|
||||
? getNumericProps(ante, bucketCount, min, max)
|
||||
: outcomeType === 'MULTIPLE_CHOICE'
|
||||
? getMultipleChoiceProps(ante, answers)
|
||||
: getFreeAnswerProps(ante)
|
||||
|
||||
const volume = outcomeType === 'BINARY' ? 0 : ante
|
||||
|
||||
const contract: Contract = removeUndefinedProps({
|
||||
id,
|
||||
slug,
|
||||
|
@ -59,19 +47,18 @@ export function getNewContract(
|
|||
creatorAvatarUrl: creator.avatarUrl,
|
||||
|
||||
question: question.trim(),
|
||||
description,
|
||||
tags: [],
|
||||
lowercaseTags: [],
|
||||
visibility,
|
||||
unlistedById: visibility === 'unlisted' ? creator.id : undefined,
|
||||
description: description.trim(),
|
||||
tags,
|
||||
lowercaseTags,
|
||||
visibility: 'public',
|
||||
isResolved: false,
|
||||
createdTime: Date.now(),
|
||||
lastUpdatedTime: Date.now(),
|
||||
closeTime,
|
||||
|
||||
volume: 0,
|
||||
volume,
|
||||
volume24Hours: 0,
|
||||
volume7Days: 0,
|
||||
elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75,
|
||||
|
||||
collectedFees: {
|
||||
creatorFee: 0,
|
||||
|
@ -83,9 +70,6 @@ export function getNewContract(
|
|||
return contract as Contract
|
||||
}
|
||||
|
||||
/*
|
||||
import { PHANTOM_ANTE } from './antes'
|
||||
import { calcDpmInitialPool } from './calculate-dpm'
|
||||
const getBinaryDpmProps = (initialProb: number, ante: number) => {
|
||||
const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } =
|
||||
calcDpmInitialPool(initialProb, ante, PHANTOM_ANTE)
|
||||
|
@ -102,40 +86,25 @@ const getBinaryDpmProps = (initialProb: number, ante: number) => {
|
|||
|
||||
return system
|
||||
}
|
||||
*/
|
||||
|
||||
const getBinaryCpmmProps = (initialProb: number, ante: number) => {
|
||||
const pool = { YES: ante, NO: ante }
|
||||
const p = initialProb / 100
|
||||
const getBinaryCpmmProps = (initialProbInt: number, ante: number) => {
|
||||
const prob = initialProbInt / 100
|
||||
|
||||
// constrain parameter to [0.1, 0.9]
|
||||
const p = prob > 0.9 ? 0.9 : prob < 0.1 ? 0.1 : prob
|
||||
|
||||
const pool = {
|
||||
YES: prob > 0.9 ? (ante * p * (prob - 1)) / ((p - 1) * prob) : ante,
|
||||
NO: prob < 0.1 ? (ante * (p - 1) * prob) / (p * (prob - 1)) : ante,
|
||||
}
|
||||
|
||||
const system: CPMM & Binary = {
|
||||
mechanism: 'cpmm-1',
|
||||
outcomeType: 'BINARY',
|
||||
totalLiquidity: ante,
|
||||
subsidyPool: 0,
|
||||
initialProbability: p,
|
||||
initialProbability: prob,
|
||||
p,
|
||||
pool: pool,
|
||||
prob: initialProb,
|
||||
probChanges: { day: 0, week: 0, month: 0 },
|
||||
}
|
||||
|
||||
return system
|
||||
}
|
||||
|
||||
const getPseudoNumericCpmmProps = (
|
||||
initialProb: number,
|
||||
ante: number,
|
||||
min: number,
|
||||
max: number,
|
||||
isLogScale: boolean
|
||||
) => {
|
||||
const system: CPMM & PseudoNumeric = {
|
||||
...getBinaryCpmmProps(initialProb, ante),
|
||||
outcomeType: 'PSEUDO_NUMERIC',
|
||||
min,
|
||||
max,
|
||||
isLogScale,
|
||||
}
|
||||
|
||||
return system
|
||||
|
@ -154,53 +123,10 @@ const getFreeAnswerProps = (ante: number) => {
|
|||
return system
|
||||
}
|
||||
|
||||
const getMultipleChoiceProps = (ante: number, answers: string[]) => {
|
||||
const numAnswers = answers.length
|
||||
const betAnte = ante / numAnswers
|
||||
const betShares = Math.sqrt(ante ** 2 / numAnswers)
|
||||
|
||||
const defaultValues = (x: any) =>
|
||||
Object.fromEntries(range(0, numAnswers).map((k) => [k, x]))
|
||||
|
||||
const system: DPM & MultipleChoice = {
|
||||
mechanism: 'dpm-2',
|
||||
outcomeType: 'MULTIPLE_CHOICE',
|
||||
pool: defaultValues(betAnte),
|
||||
totalShares: defaultValues(betShares),
|
||||
totalBets: defaultValues(betAnte),
|
||||
answers: [],
|
||||
}
|
||||
|
||||
return system
|
||||
}
|
||||
|
||||
const getNumericProps = (
|
||||
ante: number,
|
||||
bucketCount: number,
|
||||
min: number,
|
||||
max: number
|
||||
const getMultiProps = (
|
||||
outcomes: string[],
|
||||
initialProbs: number[],
|
||||
ante: number
|
||||
) => {
|
||||
const buckets = range(0, bucketCount).map((i) => i.toString())
|
||||
|
||||
const betAnte = ante / bucketCount
|
||||
const pool = Object.fromEntries(buckets.map((answer) => [answer, betAnte]))
|
||||
const totalBets = pool
|
||||
|
||||
const betShares = Math.sqrt(ante ** 2 / bucketCount)
|
||||
const totalShares = Object.fromEntries(
|
||||
buckets.map((answer) => [answer, betShares])
|
||||
)
|
||||
|
||||
const system: DPM & Numeric = {
|
||||
mechanism: 'dpm-2',
|
||||
outcomeType: 'NUMERIC',
|
||||
pool,
|
||||
totalBets,
|
||||
totalShares,
|
||||
bucketCount,
|
||||
min,
|
||||
max,
|
||||
}
|
||||
|
||||
return system
|
||||
// Not implemented.
|
||||
}
|
||||
|
|
|
@ -1,270 +0,0 @@
|
|||
import { notification_preference } from './user-notification-preferences'
|
||||
|
||||
export type Notification = {
|
||||
id: string
|
||||
userId: string
|
||||
reasonText?: string
|
||||
reason?: notification_reason_types | notification_preference
|
||||
createdTime: number
|
||||
viewTime?: number
|
||||
isSeen: boolean
|
||||
|
||||
sourceId?: string
|
||||
sourceType?: notification_source_types
|
||||
sourceUpdateType?: notification_source_update_types
|
||||
sourceContractId?: string
|
||||
sourceUserName?: string
|
||||
sourceUserUsername?: string
|
||||
sourceUserAvatarUrl?: string
|
||||
sourceText?: string
|
||||
data?: { [key: string]: any }
|
||||
|
||||
sourceContractTitle?: string
|
||||
sourceContractCreatorUsername?: string
|
||||
sourceContractSlug?: string
|
||||
|
||||
sourceSlug?: string
|
||||
sourceTitle?: string
|
||||
|
||||
isSeenOnHref?: string
|
||||
}
|
||||
|
||||
export type notification_source_types =
|
||||
| 'contract'
|
||||
| 'comment'
|
||||
| 'bet'
|
||||
| 'answer'
|
||||
| 'liquidity'
|
||||
| 'follow'
|
||||
| 'tip'
|
||||
| 'admin_message'
|
||||
| 'group'
|
||||
| 'user'
|
||||
| 'bonus'
|
||||
| 'challenge'
|
||||
| 'betting_streak_bonus'
|
||||
| 'loan'
|
||||
| 'like'
|
||||
| 'tip_and_like'
|
||||
| 'badge'
|
||||
|
||||
export type notification_source_update_types =
|
||||
| 'created'
|
||||
| 'updated'
|
||||
| 'resolved'
|
||||
| 'deleted'
|
||||
| 'closed'
|
||||
|
||||
/* Optional - if possible use a notification_preference */
|
||||
export type notification_reason_types =
|
||||
| 'tagged_user'
|
||||
| 'on_new_follow'
|
||||
| 'contract_from_followed_user'
|
||||
| 'you_referred_user'
|
||||
| 'user_joined_to_bet_on_your_market'
|
||||
| 'unique_bettors_on_your_contract'
|
||||
| 'tip_received'
|
||||
| 'bet_fill'
|
||||
| 'user_joined_from_your_group_invite'
|
||||
| 'challenge_accepted'
|
||||
| 'betting_streak_incremented'
|
||||
| 'loan_income'
|
||||
| 'liked_and_tipped_your_contract'
|
||||
| 'comment_on_your_contract'
|
||||
| 'answer_on_your_contract'
|
||||
| 'comment_on_contract_you_follow'
|
||||
| 'answer_on_contract_you_follow'
|
||||
| 'update_on_contract_you_follow'
|
||||
| 'resolution_on_contract_you_follow'
|
||||
| 'comment_on_contract_with_users_shares_in'
|
||||
| 'answer_on_contract_with_users_shares_in'
|
||||
| 'update_on_contract_with_users_shares_in'
|
||||
| 'resolution_on_contract_with_users_shares_in'
|
||||
| 'comment_on_contract_with_users_answer'
|
||||
| 'update_on_contract_with_users_answer'
|
||||
| 'resolution_on_contract_with_users_answer'
|
||||
| 'answer_on_contract_with_users_answer'
|
||||
| 'comment_on_contract_with_users_comment'
|
||||
| 'answer_on_contract_with_users_comment'
|
||||
| 'update_on_contract_with_users_comment'
|
||||
| 'resolution_on_contract_with_users_comment'
|
||||
| 'reply_to_users_answer'
|
||||
| 'reply_to_users_comment'
|
||||
| 'your_contract_closed'
|
||||
| 'subsidized_your_market'
|
||||
|
||||
type notification_descriptions = {
|
||||
[key in notification_preference]: {
|
||||
simple: string
|
||||
detailed: string
|
||||
necessary?: boolean
|
||||
}
|
||||
}
|
||||
export const NOTIFICATION_DESCRIPTIONS: notification_descriptions = {
|
||||
all_answers_on_my_markets: {
|
||||
simple: 'Answers on your markets',
|
||||
detailed: 'Answers on your own markets',
|
||||
},
|
||||
all_comments_on_my_markets: {
|
||||
simple: 'Comments on your markets',
|
||||
detailed: 'Comments on your own markets',
|
||||
},
|
||||
answers_by_followed_users_on_watched_markets: {
|
||||
simple: 'Only answers by users you follow',
|
||||
detailed: "Only answers by users you follow on markets you're watching",
|
||||
},
|
||||
answers_by_market_creator_on_watched_markets: {
|
||||
simple: 'Only answers by market creator',
|
||||
detailed: "Only answers by market creator on markets you're watching",
|
||||
},
|
||||
betting_streaks: {
|
||||
simple: `For prediction streaks`,
|
||||
detailed: `Bonuses for predictions made over consecutive days (Prediction streaks)})`,
|
||||
},
|
||||
comments_by_followed_users_on_watched_markets: {
|
||||
simple: 'Only comments by users you follow',
|
||||
detailed:
|
||||
'Only comments by users that you follow on markets that you watch',
|
||||
},
|
||||
contract_from_followed_user: {
|
||||
simple: 'New markets from users you follow',
|
||||
detailed: 'New markets from users you follow',
|
||||
},
|
||||
limit_order_fills: {
|
||||
simple: 'Limit order fills',
|
||||
detailed: 'When your limit order is filled by another user',
|
||||
},
|
||||
loan_income: {
|
||||
simple: 'Automatic loans from your predictions in unresolved markets',
|
||||
detailed:
|
||||
'Automatic loans from your predictions that are locked in unresolved markets',
|
||||
},
|
||||
market_updates_on_watched_markets: {
|
||||
simple: 'All creator updates',
|
||||
detailed: 'All market updates made by the creator',
|
||||
},
|
||||
market_updates_on_watched_markets_with_shares_in: {
|
||||
simple: "Only creator updates on markets that you're invested in",
|
||||
detailed:
|
||||
"Only updates made by the creator on markets that you're invested in",
|
||||
},
|
||||
on_new_follow: {
|
||||
simple: 'A user followed you',
|
||||
detailed: 'A user followed you',
|
||||
},
|
||||
onboarding_flow: {
|
||||
simple: 'Emails to help you get started using Manifold',
|
||||
detailed: 'Emails to help you learn how to use Manifold',
|
||||
},
|
||||
probability_updates_on_watched_markets: {
|
||||
simple: 'Large changes in probability on markets that you watch',
|
||||
detailed: 'Large changes in probability on markets that you watch',
|
||||
},
|
||||
profit_loss_updates: {
|
||||
simple: 'Weekly portfolio updates',
|
||||
detailed: 'Weekly portfolio updates',
|
||||
},
|
||||
referral_bonuses: {
|
||||
simple: 'For referring new users',
|
||||
detailed: 'Bonuses you receive from referring a new user',
|
||||
},
|
||||
resolutions_on_watched_markets: {
|
||||
simple: 'All market resolutions',
|
||||
detailed: "All resolutions on markets that you're watching",
|
||||
},
|
||||
resolutions_on_watched_markets_with_shares_in: {
|
||||
simple: "Only market resolutions that you're invested in",
|
||||
detailed:
|
||||
"Only resolutions of markets you're watching and that you're invested in",
|
||||
},
|
||||
subsidized_your_market: {
|
||||
simple: 'Your market was subsidized',
|
||||
detailed: 'When someone subsidizes your market',
|
||||
},
|
||||
tagged_user: {
|
||||
simple: 'A user tagged you',
|
||||
detailed: 'When another use tags you',
|
||||
},
|
||||
thank_you_for_purchases: {
|
||||
simple: 'Thank you notes for your purchases',
|
||||
detailed: 'Thank you notes for your purchases',
|
||||
},
|
||||
tipped_comments_on_watched_markets: {
|
||||
simple: 'Only highly tipped comments on markets that you watch',
|
||||
detailed: 'Only highly tipped comments on markets that you watch',
|
||||
},
|
||||
tips_on_your_comments: {
|
||||
simple: 'Tips on your comments',
|
||||
detailed: 'Tips on your comments',
|
||||
},
|
||||
tips_on_your_markets: {
|
||||
simple: 'Tips/Likes on your markets',
|
||||
detailed: 'Tips/Likes on your markets',
|
||||
},
|
||||
trending_markets: {
|
||||
simple: 'Weekly interesting markets',
|
||||
detailed: 'Weekly interesting markets',
|
||||
},
|
||||
unique_bettors_on_your_contract: {
|
||||
simple: 'For unique predictors on your markets',
|
||||
detailed: 'Bonuses for unique predictors on your markets',
|
||||
},
|
||||
your_contract_closed: {
|
||||
simple: 'Your market has closed and you need to resolve it (necessary)',
|
||||
detailed: 'Your market has closed and you need to resolve it (necessary)',
|
||||
necessary: true,
|
||||
},
|
||||
all_comments_on_watched_markets: {
|
||||
simple: 'All new comments',
|
||||
detailed: 'All new comments on markets you follow',
|
||||
},
|
||||
all_comments_on_contracts_with_shares_in_on_watched_markets: {
|
||||
simple: `Only on markets you're invested in`,
|
||||
detailed: `Comments on markets that you're watching and you're invested in`,
|
||||
},
|
||||
all_replies_to_my_comments_on_watched_markets: {
|
||||
simple: 'Only replies to your comments',
|
||||
detailed: "Only replies to your comments on markets you're watching",
|
||||
},
|
||||
all_replies_to_my_answers_on_watched_markets: {
|
||||
simple: 'Only replies to your answers',
|
||||
detailed: "Only replies to your answers on markets you're watching",
|
||||
},
|
||||
all_answers_on_watched_markets: {
|
||||
simple: 'All new answers',
|
||||
detailed: "All new answers on markets you're watching",
|
||||
},
|
||||
all_answers_on_contracts_with_shares_in_on_watched_markets: {
|
||||
simple: `Only on markets you're invested in`,
|
||||
detailed: `Answers on markets that you're watching and that you're invested in`,
|
||||
},
|
||||
badges_awarded: {
|
||||
simple: 'New badges awarded',
|
||||
detailed: 'New badges you have earned',
|
||||
},
|
||||
opt_out_all: {
|
||||
simple: 'Opt out of all notifications (excludes when your markets close)',
|
||||
detailed:
|
||||
'Opt out of all notifications excluding your own market closure notifications',
|
||||
},
|
||||
}
|
||||
|
||||
export type BettingStreakData = {
|
||||
streak: number
|
||||
bonusAmount: number
|
||||
}
|
||||
|
||||
export type BetFillData = {
|
||||
betOutcome: string
|
||||
creatorOutcome: string
|
||||
probability: number
|
||||
fillAmount: number
|
||||
limitOrderTotal?: number
|
||||
limitOrderRemaining?: number
|
||||
}
|
||||
|
||||
export type ContractResolutionData = {
|
||||
outcome: string
|
||||
userPayout: number
|
||||
userInvestment: number
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export const NUMERIC_BUCKET_COUNT = 200
|
||||
export const NUMERIC_FIXED_VAR = 0.005
|
||||
|
||||
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
||||
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
|
@ -2,19 +2,8 @@
|
|||
"name": "common",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"verify": "(cd .. && yarn verify)",
|
||||
"verify:dir": "npx eslint . --max-warnings 0"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"@tiptap/core": "2.0.0-beta.199",
|
||||
"@tiptap/extension-image": "2.0.0-beta.199",
|
||||
"@tiptap/extension-link": "2.0.0-beta.199",
|
||||
"@tiptap/extension-mention": "2.0.0-beta.199",
|
||||
"@tiptap/html": "2.0.0-beta.199",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.199",
|
||||
"@tiptap/suggestion": "2.0.0-beta.199",
|
||||
"lodash": "4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1,43 +1,44 @@
|
|||
import { sum, groupBy, sumBy, mapValues } from 'lodash'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import { Bet } from './bet'
|
||||
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
||||
import { DPM, FreeResponse, FullContract, Multi } from './contract'
|
||||
import {
|
||||
DPMContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
} from './contract'
|
||||
import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees'
|
||||
DPM_CREATOR_FEE,
|
||||
DPM_FEES,
|
||||
DPM_PLATFORM_FEE,
|
||||
Fees,
|
||||
noFees,
|
||||
} from './fees'
|
||||
import { addObjects } from './util/object'
|
||||
|
||||
export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => {
|
||||
export const getDpmCancelPayouts = (
|
||||
contract: FullContract<DPM, any>,
|
||||
bets: Bet[]
|
||||
) => {
|
||||
const { pool } = contract
|
||||
const poolTotal = sum(Object.values(pool))
|
||||
const poolTotal = _.sum(Object.values(pool))
|
||||
console.log('resolved N/A, pool M$', poolTotal)
|
||||
|
||||
const betSum = sumBy(bets, (b) => b.amount)
|
||||
const betSum = _.sumBy(bets, (b) => b.amount)
|
||||
|
||||
const payouts = bets.map((bet) => ({
|
||||
userId: bet.userId,
|
||||
payout: (bet.amount / betSum) * poolTotal,
|
||||
}))
|
||||
|
||||
return {
|
||||
payouts,
|
||||
creatorPayout: 0,
|
||||
liquidityPayouts: [],
|
||||
collectedFees: contract.collectedFees,
|
||||
}
|
||||
return [payouts, contract.collectedFees ?? noFees]
|
||||
}
|
||||
|
||||
export const getDpmStandardPayouts = (
|
||||
outcome: string,
|
||||
contract: DPMContract,
|
||||
contract: FullContract<DPM, any>,
|
||||
bets: Bet[]
|
||||
) => {
|
||||
const winningBets = bets.filter((bet) => bet.outcome === outcome)
|
||||
|
||||
const poolTotal = sum(Object.values(contract.pool))
|
||||
const totalShares = sumBy(winningBets, (b) => b.shares)
|
||||
const poolTotal = _.sum(Object.values(contract.pool))
|
||||
const totalShares = _.sumBy(winningBets, (b) => b.shares)
|
||||
|
||||
const payouts = winningBets.map(({ userId, amount, shares }) => {
|
||||
const winnings = (shares / totalShares) * poolTotal
|
||||
|
@ -48,66 +49,38 @@ export const getDpmStandardPayouts = (
|
|||
return { userId, profit, payout }
|
||||
})
|
||||
|
||||
const profits = sumBy(payouts, (po) => Math.max(0, po.profit))
|
||||
const profits = _.sumBy(payouts, (po) => Math.max(0, po.profit))
|
||||
const creatorFee = DPM_CREATOR_FEE * profits
|
||||
const platformFee = DPM_PLATFORM_FEE * profits
|
||||
const collectedFees = addObjects(contract.collectedFees, {
|
||||
|
||||
const finalFees: Fees = {
|
||||
creatorFee,
|
||||
platformFee,
|
||||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
liquidityPayouts: [],
|
||||
collectedFees,
|
||||
}
|
||||
}
|
||||
|
||||
export const getNumericDpmPayouts = (
|
||||
outcome: string,
|
||||
contract: DPMContract,
|
||||
bets: NumericBet[]
|
||||
) => {
|
||||
const totalShares = sumBy(bets, (bet) => bet.allOutcomeShares[outcome] ?? 0)
|
||||
const winningBets = bets.filter((bet) => !!bet.allOutcomeShares[outcome])
|
||||
const fees = addObjects<Fees>(finalFees, contract.collectedFees ?? {})
|
||||
|
||||
const poolTotal = sum(Object.values(contract.pool))
|
||||
|
||||
const payouts = winningBets.map(
|
||||
({ userId, allBetAmounts, allOutcomeShares }) => {
|
||||
const shares = allOutcomeShares[outcome] ?? 0
|
||||
const winnings = (shares / totalShares) * poolTotal
|
||||
|
||||
const amount = allBetAmounts[outcome] ?? 0
|
||||
const profit = winnings - amount
|
||||
|
||||
// profit can be negative if using phantom shares
|
||||
const payout = amount + (1 - DPM_FEES) * Math.max(0, profit)
|
||||
return { userId, profit, payout }
|
||||
}
|
||||
console.log(
|
||||
'resolved',
|
||||
outcome,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
const profits = sumBy(payouts, (po) => Math.max(0, po.profit))
|
||||
const creatorFee = DPM_CREATOR_FEE * profits
|
||||
const platformFee = DPM_PLATFORM_FEE * profits
|
||||
const collectedFees = addObjects(contract.collectedFees, {
|
||||
creatorFee,
|
||||
platformFee,
|
||||
liquidityFee: 0,
|
||||
})
|
||||
const totalPayouts = payouts
|
||||
.map(({ userId, payout }) => ({ userId, payout }))
|
||||
.concat([{ userId: contract.creatorId, payout: creatorFee }]) // add creator fee
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
liquidityPayouts: [],
|
||||
collectedFees,
|
||||
}
|
||||
return [totalPayouts, fees]
|
||||
}
|
||||
|
||||
export const getDpmMktPayouts = (
|
||||
contract: DPMContract,
|
||||
contract: FullContract<DPM, any>,
|
||||
bets: Bet[],
|
||||
resolutionProbability?: number
|
||||
) => {
|
||||
|
@ -116,7 +89,7 @@ export const getDpmMktPayouts = (
|
|||
? getDpmProbability(contract.totalShares)
|
||||
: resolutionProbability
|
||||
|
||||
const weightedShareTotal = sumBy(bets, (b) =>
|
||||
const weightedShareTotal = _.sumBy(bets, (b) =>
|
||||
b.outcome === 'YES' ? p * b.shares : (1 - p) * b.shares
|
||||
)
|
||||
|
||||
|
@ -130,62 +103,88 @@ export const getDpmMktPayouts = (
|
|||
return { userId, profit, payout }
|
||||
})
|
||||
|
||||
const profits = sumBy(payouts, (po) => Math.max(0, po.profit))
|
||||
const profits = _.sumBy(payouts, (po) => Math.max(0, po.profit))
|
||||
|
||||
const creatorFee = DPM_CREATOR_FEE * profits
|
||||
const platformFee = DPM_PLATFORM_FEE * profits
|
||||
const collectedFees = addObjects(contract.collectedFees, {
|
||||
|
||||
const finalFees: Fees = {
|
||||
creatorFee,
|
||||
platformFee,
|
||||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
liquidityPayouts: [],
|
||||
collectedFees,
|
||||
}
|
||||
|
||||
const fees = addObjects<Fees>(finalFees, contract.collectedFees ?? {})
|
||||
|
||||
console.log(
|
||||
'resolved MKT',
|
||||
p,
|
||||
'pool',
|
||||
pool,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
const totalPayouts = payouts
|
||||
.map(({ userId, payout }) => ({ userId, payout }))
|
||||
.concat([{ userId: contract.creatorId, payout: creatorFee }]) // add creator fee
|
||||
|
||||
return [totalPayouts, fees]
|
||||
}
|
||||
|
||||
export const getPayoutsMultiOutcome = (
|
||||
resolutions: { [outcome: string]: number },
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
contract: FullContract<DPM, Multi | FreeResponse>,
|
||||
bets: Bet[]
|
||||
) => {
|
||||
const poolTotal = sum(Object.values(contract.pool))
|
||||
const poolTotal = _.sum(Object.values(contract.pool))
|
||||
const winningBets = bets.filter((bet) => resolutions[bet.outcome])
|
||||
|
||||
const betsByOutcome = groupBy(winningBets, (bet) => bet.outcome)
|
||||
const sharesByOutcome = mapValues(betsByOutcome, (bets) =>
|
||||
sumBy(bets, (bet) => bet.shares)
|
||||
const betsByOutcome = _.groupBy(winningBets, (bet) => bet.outcome)
|
||||
const sharesByOutcome = _.mapValues(betsByOutcome, (bets) =>
|
||||
_.sumBy(bets, (bet) => bet.shares)
|
||||
)
|
||||
|
||||
const probTotal = sum(Object.values(resolutions))
|
||||
const probTotal = _.sum(Object.values(resolutions))
|
||||
|
||||
const payouts = winningBets.map(({ userId, outcome, amount, shares }) => {
|
||||
const prob = resolutions[outcome] / probTotal
|
||||
const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal
|
||||
const profit = winnings - amount
|
||||
|
||||
const payout = amount + (1 - DPM_FEES) * profit
|
||||
const payout = amount + (1 - DPM_FEES) * Math.max(0, profit)
|
||||
return { userId, profit, payout }
|
||||
})
|
||||
|
||||
const profits = sumBy(payouts, (po) => po.profit)
|
||||
const profits = _.sumBy(payouts, (po) => po.profit)
|
||||
|
||||
const creatorFee = DPM_CREATOR_FEE * profits
|
||||
const platformFee = DPM_PLATFORM_FEE * profits
|
||||
const collectedFees = addObjects(contract.collectedFees, {
|
||||
|
||||
const finalFees: Fees = {
|
||||
creatorFee,
|
||||
platformFee,
|
||||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
liquidityPayouts: [],
|
||||
collectedFees,
|
||||
}
|
||||
|
||||
const fees = addObjects<Fees>(finalFees, contract.collectedFees ?? noFees)
|
||||
|
||||
console.log(
|
||||
'resolved',
|
||||
resolutions,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
const totalPayouts = payouts
|
||||
.map(({ userId, payout }) => ({ userId, payout }))
|
||||
.concat([{ userId: contract.creatorId, payout: creatorFee }]) // add creator fee
|
||||
|
||||
return [totalPayouts, fees]
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { Bet } from './bet'
|
||||
import { getProbability } from './calculate'
|
||||
import { getCpmmLiquidityPoolWeights } from './calculate-cpmm'
|
||||
import { CPMMContract } from './contract'
|
||||
import { noFees } from './fees'
|
||||
import { Binary, CPMM, FixedPayouts, FullContract } from './contract'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
|
||||
export const getFixedCancelPayouts = (
|
||||
|
@ -14,21 +14,18 @@ export const getFixedCancelPayouts = (
|
|||
payout: lp.amount,
|
||||
}))
|
||||
|
||||
const payouts = bets
|
||||
return bets
|
||||
.filter((b) => !b.isAnte && !b.isLiquidityProvision)
|
||||
.map((bet) => ({
|
||||
userId: bet.userId,
|
||||
payout: bet.amount,
|
||||
}))
|
||||
|
||||
const creatorPayout = 0
|
||||
|
||||
return { payouts, creatorPayout, liquidityPayouts, collectedFees: noFees }
|
||||
.concat(liquidityPayouts)
|
||||
}
|
||||
|
||||
export const getStandardFixedPayouts = (
|
||||
outcome: string,
|
||||
contract: CPMMContract,
|
||||
contract: FullContract<FixedPayouts, Binary>,
|
||||
bets: Bet[],
|
||||
liquidities: LiquidityProvision[]
|
||||
) => {
|
||||
|
@ -39,36 +36,43 @@ export const getStandardFixedPayouts = (
|
|||
payout: shares,
|
||||
}))
|
||||
|
||||
const { collectedFees } = contract
|
||||
const creatorPayout = collectedFees.creatorFee
|
||||
const liquidityPayouts = getLiquidityPoolPayouts(
|
||||
contract,
|
||||
const creatorPayout = contract.collectedFees.creatorFee
|
||||
|
||||
console.log(
|
||||
'resolved',
|
||||
outcome,
|
||||
liquidities
|
||||
'pool',
|
||||
contract.pool[outcome],
|
||||
'payouts',
|
||||
_.sum(payouts),
|
||||
'creator fee',
|
||||
creatorPayout
|
||||
)
|
||||
|
||||
return { payouts, creatorPayout, liquidityPayouts, collectedFees }
|
||||
return payouts
|
||||
.map(({ userId, payout }) => ({ userId, payout }))
|
||||
.concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
|
||||
.concat(getLiquidityPoolPayouts(contract, outcome, liquidities))
|
||||
}
|
||||
|
||||
export const getLiquidityPoolPayouts = (
|
||||
contract: CPMMContract,
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
outcome: string,
|
||||
liquidities: LiquidityProvision[]
|
||||
) => {
|
||||
const { pool, subsidyPool } = contract
|
||||
const finalPool = pool[outcome] + (subsidyPool ?? 0)
|
||||
if (finalPool < 1e-3) return []
|
||||
const providedLiquidity = _.sumBy(liquidities, (lp) => lp.liquidity)
|
||||
|
||||
const weights = getCpmmLiquidityPoolWeights(liquidities)
|
||||
const { pool } = contract
|
||||
const finalPool = pool[outcome]
|
||||
|
||||
return Object.entries(weights).map(([providerId, weight]) => ({
|
||||
userId: providerId,
|
||||
payout: weight * finalPool,
|
||||
return liquidities.map((lp) => ({
|
||||
userId: lp.userId,
|
||||
payout: (lp.liquidity / providedLiquidity) * finalPool,
|
||||
}))
|
||||
}
|
||||
|
||||
export const getMktFixedPayouts = (
|
||||
contract: CPMMContract,
|
||||
contract: FullContract<FixedPayouts, Binary>,
|
||||
bets: Bet[],
|
||||
liquidities: LiquidityProvision[],
|
||||
resolutionProbability?: number
|
||||
|
@ -83,26 +87,37 @@ export const getMktFixedPayouts = (
|
|||
return { userId, payout: betP * shares }
|
||||
})
|
||||
|
||||
const { collectedFees } = contract
|
||||
const creatorPayout = collectedFees.creatorFee
|
||||
const liquidityPayouts = getLiquidityPoolProbPayouts(contract, p, liquidities)
|
||||
const creatorPayout = contract.collectedFees.creatorFee
|
||||
|
||||
return { payouts, creatorPayout, liquidityPayouts, collectedFees }
|
||||
console.log(
|
||||
'resolved PROB',
|
||||
p,
|
||||
'pool',
|
||||
p * contract.pool.YES + (1 - p) * contract.pool.NO,
|
||||
'payouts',
|
||||
_.sum(payouts),
|
||||
'creator fee',
|
||||
creatorPayout
|
||||
)
|
||||
|
||||
return payouts
|
||||
.map(({ userId, payout }) => ({ userId, payout }))
|
||||
.concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
|
||||
.concat(getLiquidityPoolProbPayouts(contract, p, liquidities))
|
||||
}
|
||||
|
||||
export const getLiquidityPoolProbPayouts = (
|
||||
contract: CPMMContract,
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
p: number,
|
||||
liquidities: LiquidityProvision[]
|
||||
) => {
|
||||
const { pool, subsidyPool } = contract
|
||||
const finalPool = p * pool.YES + (1 - p) * pool.NO + (subsidyPool ?? 0)
|
||||
if (finalPool < 1e-3) return []
|
||||
const providedLiquidity = _.sumBy(liquidities, (lp) => lp.liquidity)
|
||||
|
||||
const weights = getCpmmLiquidityPoolWeights(liquidities)
|
||||
const { pool } = contract
|
||||
const finalPool = p * pool.YES + (1 - p) * pool.NO
|
||||
|
||||
return Object.entries(weights).map(([providerId, weight]) => ({
|
||||
userId: providerId,
|
||||
payout: weight * finalPool,
|
||||
return liquidities.map((lp) => ({
|
||||
userId: lp.userId,
|
||||
payout: (lp.liquidity / providedLiquidity) * finalPool,
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { sumBy, groupBy, mapValues } from 'lodash'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import { Bet } from './bet'
|
||||
import {
|
||||
Binary,
|
||||
Contract,
|
||||
CPMMBinaryContract,
|
||||
DPMContract,
|
||||
PseudoNumericContract,
|
||||
DPM,
|
||||
FixedPayouts,
|
||||
FreeResponse,
|
||||
FullContract,
|
||||
Multi,
|
||||
} from './contract'
|
||||
import { Fees } from './fees'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
|
@ -13,7 +16,6 @@ import {
|
|||
getDpmCancelPayouts,
|
||||
getDpmMktPayouts,
|
||||
getDpmStandardPayouts,
|
||||
getNumericDpmPayouts,
|
||||
getPayoutsMultiOutcome,
|
||||
} from './payouts-dpm'
|
||||
import {
|
||||
|
@ -29,67 +31,50 @@ export type Payout = {
|
|||
|
||||
export const getLoanPayouts = (bets: Bet[]): Payout[] => {
|
||||
const betsWithLoans = bets.filter((bet) => bet.loanAmount)
|
||||
const betsByUser = groupBy(betsWithLoans, (bet) => bet.userId)
|
||||
const loansByUser = mapValues(betsByUser, (bets) =>
|
||||
sumBy(bets, (bet) => -(bet.loanAmount ?? 0))
|
||||
const betsByUser = _.groupBy(betsWithLoans, (bet) => bet.userId)
|
||||
const loansByUser = _.mapValues(betsByUser, (bets) =>
|
||||
_.sumBy(bets, (bet) => -(bet.loanAmount ?? 0))
|
||||
)
|
||||
return Object.entries(loansByUser).map(([userId, payout]) => ({
|
||||
userId,
|
||||
payout,
|
||||
}))
|
||||
}
|
||||
|
||||
export const groupPayoutsByUser = (payouts: Payout[]) => {
|
||||
const groups = groupBy(payouts, (payout) => payout.userId)
|
||||
return mapValues(groups, (group) => sumBy(group, (g) => g.payout))
|
||||
}
|
||||
|
||||
export type PayoutInfo = {
|
||||
payouts: Payout[]
|
||||
creatorPayout: number
|
||||
liquidityPayouts: Payout[]
|
||||
collectedFees: Fees
|
||||
return _.toPairs(loansByUser).map(([userId, payout]) => ({ userId, payout }))
|
||||
}
|
||||
|
||||
export const getPayouts = (
|
||||
outcome: string | undefined,
|
||||
outcome: string,
|
||||
resolutions: {
|
||||
[outcome: string]: number
|
||||
},
|
||||
contract: Contract,
|
||||
bets: Bet[],
|
||||
liquidities: LiquidityProvision[],
|
||||
resolutions?: {
|
||||
[outcome: string]: number
|
||||
},
|
||||
resolutionProbability?: number
|
||||
): PayoutInfo => {
|
||||
if (
|
||||
contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
) {
|
||||
return getFixedPayouts(
|
||||
): [Payout[], Fees] => {
|
||||
if (contract.mechanism === 'cpmm-1' && contract.outcomeType === 'BINARY') {
|
||||
const payouts = getFixedPayouts(
|
||||
outcome,
|
||||
contract,
|
||||
bets,
|
||||
liquidities,
|
||||
resolutionProbability
|
||||
)
|
||||
return [payouts, contract.collectedFees]
|
||||
}
|
||||
|
||||
return getDpmPayouts(
|
||||
outcome,
|
||||
resolutions,
|
||||
contract,
|
||||
bets,
|
||||
resolutions,
|
||||
resolutionProbability
|
||||
)
|
||||
}
|
||||
|
||||
export const getFixedPayouts = (
|
||||
outcome: string | undefined,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
outcome: string,
|
||||
contract: FullContract<FixedPayouts, Binary>,
|
||||
bets: Bet[],
|
||||
liquidities: LiquidityProvision[],
|
||||
resolutionProbability?: number
|
||||
) => {
|
||||
): Payout[] => {
|
||||
switch (outcome) {
|
||||
case 'YES':
|
||||
case 'NO':
|
||||
|
@ -108,36 +93,43 @@ export const getFixedPayouts = (
|
|||
}
|
||||
|
||||
export const getDpmPayouts = (
|
||||
outcome: string | undefined,
|
||||
contract: DPMContract,
|
||||
bets: Bet[],
|
||||
resolutions?: {
|
||||
outcome: string,
|
||||
resolutions: {
|
||||
[outcome: string]: number
|
||||
},
|
||||
contract: Contract,
|
||||
bets: Bet[],
|
||||
resolutionProbability?: number
|
||||
): PayoutInfo => {
|
||||
) => {
|
||||
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
||||
const { outcomeType } = contract
|
||||
|
||||
switch (outcome) {
|
||||
case 'YES':
|
||||
case 'NO':
|
||||
return getDpmStandardPayouts(outcome, contract, openBets)
|
||||
return getDpmStandardPayouts(outcome, contract, openBets) as [
|
||||
Payout[],
|
||||
Fees
|
||||
]
|
||||
|
||||
case 'MKT':
|
||||
return outcomeType === 'FREE_RESPONSE' ||
|
||||
outcomeType === 'MULTIPLE_CHOICE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
? getPayoutsMultiOutcome(resolutions!, contract, openBets)
|
||||
: getDpmMktPayouts(contract, openBets, resolutionProbability)
|
||||
return contract.outcomeType === 'FREE_RESPONSE'
|
||||
? (getPayoutsMultiOutcome(
|
||||
resolutions,
|
||||
contract as FullContract<DPM, Multi | FreeResponse>,
|
||||
openBets
|
||||
) as [Payout[], Fees])
|
||||
: (getDpmMktPayouts(contract, openBets, resolutionProbability) as [
|
||||
Payout[],
|
||||
Fees
|
||||
])
|
||||
case 'CANCEL':
|
||||
case undefined:
|
||||
return getDpmCancelPayouts(contract, openBets)
|
||||
return getDpmCancelPayouts(contract, openBets) as [Payout[], Fees]
|
||||
|
||||
default:
|
||||
if (outcomeType === 'NUMERIC')
|
||||
return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[])
|
||||
|
||||
// Outcome is a free response answer id.
|
||||
return getDpmStandardPayouts(outcome, contract, openBets)
|
||||
return getDpmStandardPayouts(outcome, contract, openBets) as [
|
||||
Payout[],
|
||||
Fees
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import { JSONContent } from '@tiptap/core'
|
||||
|
||||
export type Post = {
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
content: JSONContent
|
||||
creatorId: string // User id
|
||||
createdTime: number
|
||||
slug: string
|
||||
|
||||
// denormalized user fields
|
||||
creatorName: string
|
||||
creatorUsername: string
|
||||
creatorAvatarUrl?: string
|
||||
|
||||
likedByUserIds?: string[]
|
||||
likedByUserCount?: number
|
||||
}
|
||||
|
||||
export type DateDoc = Post & {
|
||||
bounty: number
|
||||
birthday: number
|
||||
type: 'date-doc'
|
||||
contractSlug: string
|
||||
}
|
||||
|
||||
export const MAX_POST_TITLE_LENGTH = 480
|
||||
export const MAX_POST_SUBTITLE_LENGTH = 480
|
|
@ -1,48 +0,0 @@
|
|||
import { BinaryContract, PseudoNumericContract } from './contract'
|
||||
import { formatLargeNumber, formatPercent } from './util/format'
|
||||
|
||||
export function formatNumericProbability(
|
||||
p: number,
|
||||
contract: PseudoNumericContract
|
||||
) {
|
||||
const value = getMappedValue(contract)(p)
|
||||
return formatLargeNumber(value)
|
||||
}
|
||||
|
||||
export const getMappedValue =
|
||||
(contract: PseudoNumericContract | BinaryContract) => (p: number) => {
|
||||
if (contract.outcomeType === 'BINARY') return p
|
||||
|
||||
const { min, max, isLogScale } = contract
|
||||
|
||||
if (isLogScale) {
|
||||
const logValue = p * Math.log10(max - min + 1)
|
||||
return 10 ** logValue + min - 1
|
||||
}
|
||||
|
||||
return p * (max - min) + min
|
||||
}
|
||||
|
||||
export const getFormattedMappedValue =
|
||||
(contract: PseudoNumericContract | BinaryContract) => (p: number) => {
|
||||
if (contract.outcomeType === 'BINARY') return formatPercent(p)
|
||||
|
||||
const value = getMappedValue(contract)(p)
|
||||
return formatLargeNumber(value)
|
||||
}
|
||||
|
||||
export const getPseudoProbability = (
|
||||
value: number,
|
||||
min: number,
|
||||
max: number,
|
||||
isLogScale = false
|
||||
) => {
|
||||
if (value < min) return 0
|
||||
if (value > max) return 1
|
||||
|
||||
if (isLogScale) {
|
||||
return Math.log10(value - min + 1) / Math.log10(max - min + 1)
|
||||
}
|
||||
|
||||
return (value - min) / (max - min)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
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))
|
||||
}
|
94
common/recommended-contracts.ts
Normal file
94
common/recommended-contracts.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import * as _ from 'lodash'
|
||||
import { Contract } from './contract'
|
||||
import { filterDefined } from './util/array'
|
||||
import { addObjects } from './util/object'
|
||||
|
||||
export const getRecommendedContracts = (
|
||||
contractsById: { [contractId: string]: Contract },
|
||||
yourBetOnContractIds: string[]
|
||||
) => {
|
||||
const contracts = Object.values(contractsById)
|
||||
const yourContracts = filterDefined(
|
||||
yourBetOnContractIds.map((contractId) => contractsById[contractId])
|
||||
)
|
||||
|
||||
const yourContractIds = new Set(yourContracts.map((c) => c.id))
|
||||
const notYourContracts = contracts.filter((c) => !yourContractIds.has(c.id))
|
||||
|
||||
const yourWordFrequency = contractsToWordFrequency(yourContracts)
|
||||
const otherWordFrequency = contractsToWordFrequency(notYourContracts)
|
||||
const words = _.union(
|
||||
Object.keys(yourWordFrequency),
|
||||
Object.keys(otherWordFrequency)
|
||||
)
|
||||
|
||||
const yourWeightedFrequency = _.fromPairs(
|
||||
_.map(words, (word) => {
|
||||
const [yourFreq, otherFreq] = [
|
||||
yourWordFrequency[word] ?? 0,
|
||||
otherWordFrequency[word] ?? 0,
|
||||
]
|
||||
|
||||
const score = yourFreq / (yourFreq + otherFreq + 0.0001)
|
||||
|
||||
return [word, score]
|
||||
})
|
||||
)
|
||||
|
||||
// console.log(
|
||||
// 'your weighted frequency',
|
||||
// _.sortBy(_.toPairs(yourWeightedFrequency), ([, freq]) => -freq)
|
||||
// )
|
||||
|
||||
const scoredContracts = contracts.map((contract) => {
|
||||
const wordFrequency = contractToWordFrequency(contract)
|
||||
|
||||
const score = _.sumBy(Object.keys(wordFrequency), (word) => {
|
||||
const wordFreq = wordFrequency[word] ?? 0
|
||||
const weight = yourWeightedFrequency[word] ?? 0
|
||||
return wordFreq * weight
|
||||
})
|
||||
|
||||
return {
|
||||
contract,
|
||||
score,
|
||||
}
|
||||
})
|
||||
|
||||
return _.sortBy(scoredContracts, (scored) => -scored.score).map(
|
||||
(scored) => scored.contract
|
||||
)
|
||||
}
|
||||
|
||||
const contractToText = (contract: Contract) => {
|
||||
const { description, question, tags, creatorUsername } = contract
|
||||
return `${creatorUsername} ${question} ${tags.join(' ')} ${description}`
|
||||
}
|
||||
|
||||
const getWordsCount = (text: string) => {
|
||||
const normalizedText = text.replace(/[^a-zA-Z]/g, ' ').toLowerCase()
|
||||
const words = normalizedText.split(' ').filter((word) => word)
|
||||
|
||||
const counts: { [word: string]: number } = {}
|
||||
for (const word of words) {
|
||||
if (counts[word]) counts[word]++
|
||||
else counts[word] = 1
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
const toFrequency = (counts: { [word: string]: number }) => {
|
||||
const total = _.sum(Object.values(counts))
|
||||
return _.mapValues(counts, (count) => count / total)
|
||||
}
|
||||
|
||||
const contractToWordFrequency = (contract: Contract) =>
|
||||
toFrequency(getWordsCount(contractToText(contract)))
|
||||
|
||||
const contractsToWordFrequency = (contracts: Contract[]) => {
|
||||
const frequencySum = contracts
|
||||
.map(contractToWordFrequency)
|
||||
.reduce(addObjects, {})
|
||||
|
||||
return toFrequency(frequencySum)
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import { partition, sumBy } from 'lodash'
|
||||
|
||||
import { Bet } from './bet'
|
||||
import { getProbability } from './calculate'
|
||||
import { CPMMContract } from './contract'
|
||||
import { noFees } from './fees'
|
||||
import { CandidateBet } from './new-bet'
|
||||
|
||||
type RedeemableBet = Pick<Bet, 'outcome' | 'shares' | 'loanAmount'>
|
||||
|
||||
export const getRedeemableAmount = (bets: RedeemableBet[]) => {
|
||||
const [yesBets, noBets] = partition(bets, (b) => b.outcome === 'YES')
|
||||
const yesShares = sumBy(yesBets, (b) => b.shares)
|
||||
const noShares = sumBy(noBets, (b) => b.shares)
|
||||
const shares = Math.max(Math.min(yesShares, noShares), 0)
|
||||
const soldFrac =
|
||||
shares > 0
|
||||
? Math.min(yesShares, noShares) / Math.max(yesShares, noShares)
|
||||
: 0
|
||||
const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
|
||||
const loanPayment = loanAmount * soldFrac
|
||||
const netAmount = shares - loanPayment
|
||||
return { shares, loanPayment, netAmount }
|
||||
}
|
||||
|
||||
export const getRedemptionBets = (
|
||||
shares: number,
|
||||
loanPayment: number,
|
||||
contract: CPMMContract
|
||||
) => {
|
||||
const p = getProbability(contract)
|
||||
const createdTime = Date.now()
|
||||
const yesBet: CandidateBet = {
|
||||
contractId: contract.id,
|
||||
amount: p * -shares,
|
||||
shares: -shares,
|
||||
loanAmount: loanPayment ? -loanPayment / 2 : 0,
|
||||
outcome: 'YES',
|
||||
probBefore: p,
|
||||
probAfter: p,
|
||||
createdTime,
|
||||
isRedemption: true,
|
||||
fees: noFees,
|
||||
}
|
||||
const noBet: CandidateBet = {
|
||||
contractId: contract.id,
|
||||
amount: (1 - p) * -shares,
|
||||
shares: -shares,
|
||||
loanAmount: loanPayment ? -loanPayment / 2 : 0,
|
||||
outcome: 'NO',
|
||||
probBefore: p,
|
||||
probAfter: p,
|
||||
createdTime,
|
||||
isRedemption: true,
|
||||
fees: noFees,
|
||||
}
|
||||
return [yesBet, noBet]
|
||||
}
|
|
@ -1,19 +1,13 @@
|
|||
import { groupBy, sumBy, mapValues, keyBy, sortBy } from 'lodash'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
import { Bet } from './bet'
|
||||
import { getContractBetMetrics, resolvedPayout } from './calculate'
|
||||
import { Contract } from './contract'
|
||||
import { ContractComment } from './comment'
|
||||
import { Binary, Contract, FullContract } from './contract'
|
||||
import { getPayouts } from './payouts'
|
||||
|
||||
export function scoreCreators(contracts: Contract[]) {
|
||||
const creatorScore = mapValues(
|
||||
groupBy(contracts, ({ creatorId }) => creatorId),
|
||||
(contracts) =>
|
||||
sumBy(
|
||||
contracts.map((contract) => {
|
||||
return contract.volume
|
||||
})
|
||||
)
|
||||
export function scoreCreators(contracts: Contract[], bets: Bet[][]) {
|
||||
const creatorScore = _.mapValues(
|
||||
_.groupBy(contracts, ({ creatorId }) => creatorId),
|
||||
(contracts) => _.sumBy(contracts, ({ pool }) => pool.YES + pool.NO)
|
||||
)
|
||||
|
||||
return creatorScore
|
||||
|
@ -30,12 +24,46 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
|
|||
return userScores
|
||||
}
|
||||
|
||||
export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
||||
const betsByUser = groupBy(bets, (bet) => bet.userId)
|
||||
return mapValues(
|
||||
betsByUser,
|
||||
(bets) => getContractBetMetrics(contract, bets).profit
|
||||
export function scoreUsersByContract(
|
||||
contract: FullContract<any, Binary>,
|
||||
bets: Bet[]
|
||||
) {
|
||||
const { resolution, resolutionProbability } = contract
|
||||
|
||||
const [closedBets, openBets] = _.partition(
|
||||
bets,
|
||||
(bet) => bet.isSold || bet.sale
|
||||
)
|
||||
const [resolvePayouts] = getPayouts(
|
||||
resolution,
|
||||
{},
|
||||
contract,
|
||||
openBets,
|
||||
[],
|
||||
resolutionProbability
|
||||
)
|
||||
|
||||
const salePayouts = closedBets.map((bet) => {
|
||||
const { userId, sale } = bet
|
||||
return { userId, payout: sale ? sale.amount : 0 }
|
||||
})
|
||||
|
||||
const investments = bets
|
||||
.filter((bet) => !bet.sale)
|
||||
.map((bet) => {
|
||||
const { userId, amount, loanAmount } = bet
|
||||
const payout = -amount - (loanAmount ?? 0)
|
||||
return { userId, payout }
|
||||
})
|
||||
|
||||
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
|
||||
|
||||
const userScore = _.mapValues(
|
||||
_.groupBy(netPayouts, (payout) => payout.userId),
|
||||
(payouts) => _.sumBy(payouts, ({ payout }) => payout)
|
||||
)
|
||||
|
||||
return userScore
|
||||
}
|
||||
|
||||
export function addUserScores(
|
||||
|
@ -47,47 +75,3 @@ export function addUserScores(
|
|||
dest[userId] += score
|
||||
}
|
||||
}
|
||||
|
||||
export function scoreCommentorsAndBettors(
|
||||
contract: Contract,
|
||||
bets: Bet[],
|
||||
comments: ContractComment[]
|
||||
) {
|
||||
const commentsById = keyBy(comments, 'id')
|
||||
const betsById = keyBy(bets, 'id')
|
||||
|
||||
// If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit
|
||||
// Otherwise, we record the profit at resolution time
|
||||
const profitById: Record<string, number> = {}
|
||||
for (const bet of bets) {
|
||||
if (bet.sale) {
|
||||
const originalBet = betsById[bet.sale.betId]
|
||||
const profit = bet.sale.amount - originalBet.amount
|
||||
profitById[bet.id] = profit
|
||||
profitById[originalBet.id] = profit
|
||||
} else {
|
||||
profitById[bet.id] = resolvedPayout(contract, bet) - bet.amount
|
||||
}
|
||||
}
|
||||
|
||||
// Now find the betId with the highest profit
|
||||
const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id
|
||||
const topBettor = betsById[topBetId]?.userName
|
||||
|
||||
// And also the commentId of the comment with the highest profit
|
||||
const topCommentId = sortBy(
|
||||
comments,
|
||||
(c) => c.betId && -profitById[c.betId]
|
||||
)[0]?.id
|
||||
const topCommentBetId = commentsById[topCommentId]?.betId
|
||||
|
||||
return {
|
||||
topCommentId,
|
||||
topBetId,
|
||||
topBettor,
|
||||
profitById,
|
||||
commentsById,
|
||||
betsById,
|
||||
topCommentBetId,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { Bet, LimitBet } from './bet'
|
||||
import { Bet } from './bet'
|
||||
import {
|
||||
getDpmProbability,
|
||||
calculateDpmShareValue,
|
||||
deductDpmFees,
|
||||
getDpmOutcomeProbability,
|
||||
} from './calculate-dpm'
|
||||
import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm'
|
||||
import { CPMMContract, DPMContract } from './contract'
|
||||
import { Binary, DPM, CPMM, FullContract } from './contract'
|
||||
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
|
||||
import { sumBy } from 'lodash'
|
||||
import { User } from './user'
|
||||
|
||||
export type CandidateBet<T extends Bet> = Omit<
|
||||
T,
|
||||
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
|
||||
>
|
||||
|
||||
export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
|
||||
export const getSellBetInfo = (
|
||||
user: User,
|
||||
bet: Bet,
|
||||
contract: FullContract<DPM, any>,
|
||||
newBetId: string
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
const { id: betId, amount, shares, outcome, loanAmount } = bet
|
||||
|
||||
|
@ -29,8 +29,8 @@ export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
|
|||
|
||||
const newTotalBets = { ...totalBets, [outcome]: totalBets[outcome] - amount }
|
||||
|
||||
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
|
||||
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
|
||||
const probBefore = getDpmProbability(totalShares)
|
||||
const probAfter = getDpmProbability(newTotalShares)
|
||||
|
||||
const profit = adjShareValue - amount
|
||||
|
||||
|
@ -54,7 +54,9 @@ export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
|
|||
creatorFee
|
||||
)
|
||||
|
||||
const newBet: CandidateBet<Bet> = {
|
||||
const newBet: Bet = {
|
||||
id: newBetId,
|
||||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount: -adjShareValue,
|
||||
shares: -shares,
|
||||
|
@ -67,41 +69,41 @@ export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
|
|||
betId,
|
||||
},
|
||||
fees,
|
||||
loanAmount: -(loanAmount ?? 0),
|
||||
}
|
||||
|
||||
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,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number },
|
||||
loanPaid: number
|
||||
contract: FullContract<CPMM, Binary>,
|
||||
prevLoanAmount: number,
|
||||
newBetId: string
|
||||
) => {
|
||||
const { pool, p } = contract
|
||||
|
||||
const { saleValue, cpmmState, fees, makers, takers, ordersToCancel } = calculateCpmmSale(
|
||||
const { saleValue, newPool, newP, fees } = calculateCpmmSale(
|
||||
contract,
|
||||
shares,
|
||||
outcome,
|
||||
unfilledBets,
|
||||
balanceByUserId,
|
||||
outcome
|
||||
)
|
||||
|
||||
const probBefore = getCpmmProbability(pool, p)
|
||||
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
const loanPaid = Math.min(prevLoanAmount, saleValue)
|
||||
const netAmount = saleValue - loanPaid
|
||||
|
||||
const takerAmount = sumBy(takers, 'amount')
|
||||
const takerShares = sumBy(takers, 'shares')
|
||||
const probBefore = getCpmmProbability(pool, p)
|
||||
const probAfter = getCpmmProbability(newPool, p)
|
||||
|
||||
console.log(
|
||||
'SELL M$',
|
||||
|
@ -113,29 +115,27 @@ export const getCpmmSellBetInfo = (
|
|||
fees.creatorFee
|
||||
)
|
||||
|
||||
const newBet: CandidateBet<Bet> = {
|
||||
const newBet: Bet = {
|
||||
id: newBetId,
|
||||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount: takerAmount,
|
||||
shares: takerShares,
|
||||
amount: -saleValue,
|
||||
shares: -shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
probAfter,
|
||||
createdTime: Date.now(),
|
||||
loanAmount: -loanPaid,
|
||||
fees,
|
||||
fills: takers,
|
||||
isFilled: true,
|
||||
isCancelled: false,
|
||||
orderAmount: takerAmount,
|
||||
}
|
||||
|
||||
const newBalance = user.balance + netAmount
|
||||
|
||||
return {
|
||||
newBet,
|
||||
newPool: cpmmState.pool,
|
||||
newP: cpmmState.p,
|
||||
newPool,
|
||||
newP,
|
||||
newBalance,
|
||||
fees,
|
||||
makers,
|
||||
takers,
|
||||
ordersToCancel
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
export type Stats = {
|
||||
startDate: number
|
||||
dailyActiveUsers: number[]
|
||||
dailyActiveUsersWeeklyAvg: number[]
|
||||
weeklyActiveUsers: number[]
|
||||
monthlyActiveUsers: number[]
|
||||
d1: number[]
|
||||
d1WeeklyAvg: number[]
|
||||
nd1: number[]
|
||||
nd1WeeklyAvg: number[]
|
||||
nw1: number[]
|
||||
dailyBetCounts: number[]
|
||||
dailyContractCounts: number[]
|
||||
dailyCommentCounts: number[]
|
||||
dailySignups: number[]
|
||||
weekOnWeekRetention: number[]
|
||||
monthlyRetention: number[]
|
||||
dailyActivationRate: number[]
|
||||
dailyActivationRateWeeklyAvg: number[]
|
||||
manaBet: {
|
||||
daily: number[]
|
||||
weekly: number[]
|
||||
monthly: number[]
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "../",
|
||||
"composite": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitReturns": true,
|
||||
"outDir": "lib",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "es2017"
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
140
common/txn.ts
140
common/txn.ts
|
@ -1,140 +0,0 @@
|
|||
// A txn (pronounced "texan") respresents a payment between two ids on Manifold
|
||||
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
|
||||
type AnyTxnType =
|
||||
| Donation
|
||||
| Tip
|
||||
| Manalink
|
||||
| Referral
|
||||
| UniqueBettorBonus
|
||||
| BettingStreakBonus
|
||||
| CancelUniqueBettorBonus
|
||||
| CommentBountyRefund
|
||||
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
|
||||
|
||||
export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||
id: string
|
||||
createdTime: number
|
||||
|
||||
fromId: string
|
||||
fromType: SourceType
|
||||
|
||||
toId: string
|
||||
toType: SourceType
|
||||
|
||||
amount: number
|
||||
token: 'M$' // | 'USD' | MarketOutcome
|
||||
|
||||
category:
|
||||
| 'CHARITY'
|
||||
| 'MANALINK'
|
||||
| 'TIP'
|
||||
| 'REFERRAL'
|
||||
| 'UNIQUE_BETTOR_BONUS'
|
||||
| 'BETTING_STREAK_BONUS'
|
||||
| 'CANCEL_UNIQUE_BETTOR_BONUS'
|
||||
| 'COMMENT_BOUNTY'
|
||||
| 'REFUND_COMMENT_BOUNTY'
|
||||
|
||||
// Any extra data
|
||||
data?: { [key: string]: any }
|
||||
|
||||
// Human-readable description
|
||||
description?: string
|
||||
} & T
|
||||
|
||||
type Donation = {
|
||||
fromType: 'USER'
|
||||
toType: 'CHARITY'
|
||||
category: 'CHARITY'
|
||||
}
|
||||
|
||||
type Tip = {
|
||||
fromType: 'USER'
|
||||
toType: 'USER'
|
||||
category: 'TIP'
|
||||
data: {
|
||||
commentId: string
|
||||
contractId?: string
|
||||
groupId?: string
|
||||
}
|
||||
}
|
||||
|
||||
type Manalink = {
|
||||
fromType: 'USER'
|
||||
toType: 'USER'
|
||||
category: 'MANALINK'
|
||||
}
|
||||
|
||||
type Referral = {
|
||||
fromType: 'BANK'
|
||||
toType: 'USER'
|
||||
category: 'REFERRAL'
|
||||
}
|
||||
|
||||
type UniqueBettorBonus = {
|
||||
fromType: 'BANK'
|
||||
toType: 'USER'
|
||||
category: 'UNIQUE_BETTOR_BONUS'
|
||||
data: {
|
||||
contractId: string
|
||||
uniqueNewBettorId?: string
|
||||
// Old unique bettor bonus txns stored all unique bettor ids
|
||||
uniqueBettorIds?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
type BettingStreakBonus = {
|
||||
fromType: 'BANK'
|
||||
toType: 'USER'
|
||||
category: 'BETTING_STREAK_BONUS'
|
||||
data: {
|
||||
currentBettingStreak?: number
|
||||
}
|
||||
}
|
||||
|
||||
type CancelUniqueBettorBonus = {
|
||||
fromType: 'USER'
|
||||
toType: 'BANK'
|
||||
category: 'CANCEL_UNIQUE_BETTOR_BONUS'
|
||||
data: {
|
||||
contractId: string
|
||||
}
|
||||
}
|
||||
|
||||
type CommentBountyDeposit = {
|
||||
fromType: 'USER'
|
||||
toType: 'BANK'
|
||||
category: 'COMMENT_BOUNTY'
|
||||
data: {
|
||||
contractId: string
|
||||
}
|
||||
}
|
||||
|
||||
type CommentBountyWithdrawal = {
|
||||
fromType: 'BANK'
|
||||
toType: 'USER'
|
||||
category: 'COMMENT_BOUNTY'
|
||||
data: {
|
||||
contractId: string
|
||||
commentId: string
|
||||
}
|
||||
}
|
||||
|
||||
type CommentBountyRefund = {
|
||||
fromType: 'BANK'
|
||||
toType: 'USER'
|
||||
category: 'REFUND_COMMENT_BOUNTY'
|
||||
data: {
|
||||
contractId: string
|
||||
}
|
||||
}
|
||||
|
||||
export type DonationTxn = Txn & Donation
|
||||
export type TipTxn = Txn & Tip
|
||||
export type ManalinkTxn = Txn & Manalink
|
||||
export type ReferralTxn = Txn & Referral
|
||||
export type BettingStreakBonusTxn = Txn & BettingStreakBonus
|
||||
export type UniqueBettorBonusTxn = Txn & UniqueBettorBonus
|
||||
export type CancelUniqueBettorBonusTxn = Txn & CancelUniqueBettorBonus
|
||||
export type CommentBountyDepositTxn = Txn & CommentBountyDeposit
|
||||
export type CommentBountyWithdrawalTxn = Txn & CommentBountyWithdrawal
|
|
@ -1,222 +0,0 @@
|
|||
import { filterDefined } from './util/array'
|
||||
import { notification_reason_types } from './notification'
|
||||
import { getFunctionUrl } from './api'
|
||||
import { DOMAIN } from './envs/constants'
|
||||
import { PrivateUser } from './user'
|
||||
|
||||
export type notification_destination_types = 'email' | 'browser'
|
||||
export type notification_preference = keyof notification_preferences
|
||||
export type notification_preferences = {
|
||||
// Watched Markets
|
||||
all_comments_on_watched_markets: notification_destination_types[]
|
||||
all_answers_on_watched_markets: notification_destination_types[]
|
||||
|
||||
// Comments
|
||||
tipped_comments_on_watched_markets: notification_destination_types[]
|
||||
comments_by_followed_users_on_watched_markets: notification_destination_types[]
|
||||
all_replies_to_my_comments_on_watched_markets: notification_destination_types[]
|
||||
all_replies_to_my_answers_on_watched_markets: notification_destination_types[]
|
||||
all_comments_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
|
||||
|
||||
// Answers
|
||||
answers_by_followed_users_on_watched_markets: notification_destination_types[]
|
||||
answers_by_market_creator_on_watched_markets: notification_destination_types[]
|
||||
all_answers_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
|
||||
|
||||
// On users' markets
|
||||
your_contract_closed: notification_destination_types[]
|
||||
all_comments_on_my_markets: notification_destination_types[]
|
||||
all_answers_on_my_markets: notification_destination_types[]
|
||||
subsidized_your_market: notification_destination_types[]
|
||||
|
||||
// Market updates
|
||||
resolutions_on_watched_markets: notification_destination_types[]
|
||||
resolutions_on_watched_markets_with_shares_in: notification_destination_types[]
|
||||
market_updates_on_watched_markets: notification_destination_types[]
|
||||
market_updates_on_watched_markets_with_shares_in: notification_destination_types[]
|
||||
probability_updates_on_watched_markets: notification_destination_types[]
|
||||
|
||||
// Balance Changes
|
||||
loan_income: notification_destination_types[]
|
||||
betting_streaks: notification_destination_types[]
|
||||
referral_bonuses: notification_destination_types[]
|
||||
unique_bettors_on_your_contract: notification_destination_types[]
|
||||
tips_on_your_comments: notification_destination_types[]
|
||||
tips_on_your_markets: notification_destination_types[]
|
||||
limit_order_fills: notification_destination_types[]
|
||||
|
||||
// General
|
||||
tagged_user: notification_destination_types[]
|
||||
on_new_follow: notification_destination_types[]
|
||||
contract_from_followed_user: notification_destination_types[]
|
||||
trending_markets: notification_destination_types[]
|
||||
profit_loss_updates: notification_destination_types[]
|
||||
onboarding_flow: notification_destination_types[]
|
||||
thank_you_for_purchases: notification_destination_types[]
|
||||
badges_awarded: notification_destination_types[]
|
||||
opt_out_all: notification_destination_types[]
|
||||
// When adding a new notification preference, use add-new-notification-preference.ts to existing users
|
||||
}
|
||||
|
||||
export const getDefaultNotificationPreferences = (
|
||||
userId: string,
|
||||
privateUser?: PrivateUser,
|
||||
noEmails?: boolean
|
||||
) => {
|
||||
const constructPref = (browserIf: boolean, emailIf: boolean) => {
|
||||
const browser = browserIf ? 'browser' : undefined
|
||||
const email = noEmails ? undefined : emailIf ? 'email' : undefined
|
||||
return filterDefined([browser, email]) as notification_destination_types[]
|
||||
}
|
||||
const defaults: notification_preferences = {
|
||||
// Watched Markets
|
||||
all_comments_on_watched_markets: constructPref(true, false),
|
||||
all_answers_on_watched_markets: constructPref(true, false),
|
||||
|
||||
// Comments
|
||||
tips_on_your_comments: constructPref(true, true),
|
||||
comments_by_followed_users_on_watched_markets: constructPref(true, true),
|
||||
all_replies_to_my_comments_on_watched_markets: constructPref(true, true),
|
||||
all_replies_to_my_answers_on_watched_markets: constructPref(true, true),
|
||||
all_comments_on_contracts_with_shares_in_on_watched_markets: constructPref(
|
||||
true,
|
||||
false
|
||||
),
|
||||
|
||||
// Answers
|
||||
answers_by_followed_users_on_watched_markets: constructPref(true, true),
|
||||
answers_by_market_creator_on_watched_markets: constructPref(true, true),
|
||||
all_answers_on_contracts_with_shares_in_on_watched_markets: constructPref(
|
||||
true,
|
||||
true
|
||||
),
|
||||
|
||||
// On users' markets
|
||||
your_contract_closed: constructPref(true, true), // High priority
|
||||
all_comments_on_my_markets: constructPref(true, true),
|
||||
all_answers_on_my_markets: constructPref(true, true),
|
||||
subsidized_your_market: constructPref(true, true),
|
||||
|
||||
// Market updates
|
||||
resolutions_on_watched_markets: constructPref(true, false),
|
||||
market_updates_on_watched_markets: constructPref(true, false),
|
||||
market_updates_on_watched_markets_with_shares_in: constructPref(
|
||||
true,
|
||||
false
|
||||
),
|
||||
resolutions_on_watched_markets_with_shares_in: constructPref(true, true),
|
||||
|
||||
//Balance Changes
|
||||
loan_income: constructPref(true, false),
|
||||
betting_streaks: constructPref(true, false),
|
||||
referral_bonuses: constructPref(true, true),
|
||||
unique_bettors_on_your_contract: constructPref(true, true),
|
||||
tipped_comments_on_watched_markets: constructPref(true, true),
|
||||
tips_on_your_markets: constructPref(true, true),
|
||||
limit_order_fills: constructPref(true, false),
|
||||
|
||||
// General
|
||||
tagged_user: constructPref(true, true),
|
||||
on_new_follow: constructPref(true, true),
|
||||
contract_from_followed_user: constructPref(true, true),
|
||||
trending_markets: constructPref(false, true),
|
||||
profit_loss_updates: constructPref(false, true),
|
||||
probability_updates_on_watched_markets: constructPref(true, false),
|
||||
thank_you_for_purchases: constructPref(false, false),
|
||||
onboarding_flow: constructPref(false, false),
|
||||
|
||||
opt_out_all: [],
|
||||
badges_awarded: constructPref(true, false),
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
// Adding a new key:value here is optional, you can just use a key of notification_subscription_types
|
||||
// You might want to add a key:value here if there will be multiple notification reasons that map to the same
|
||||
// subscription type, i.e. 'comment_on_contract_you_follow' and 'comment_on_contract_with_users_answer' both map to
|
||||
// 'all_comments_on_watched_markets' subscription type
|
||||
// TODO: perhaps better would be to map notification_subscription_types to arrays of notification_reason_types
|
||||
const notificationReasonToSubscriptionType: Partial<
|
||||
Record<notification_reason_types, notification_preference>
|
||||
> = {
|
||||
you_referred_user: 'referral_bonuses',
|
||||
user_joined_to_bet_on_your_market: 'referral_bonuses',
|
||||
tip_received: 'tips_on_your_comments',
|
||||
bet_fill: 'limit_order_fills',
|
||||
user_joined_from_your_group_invite: 'referral_bonuses',
|
||||
challenge_accepted: 'limit_order_fills',
|
||||
betting_streak_incremented: 'betting_streaks',
|
||||
liked_and_tipped_your_contract: 'tips_on_your_markets',
|
||||
comment_on_your_contract: 'all_comments_on_my_markets',
|
||||
answer_on_your_contract: 'all_answers_on_my_markets',
|
||||
comment_on_contract_you_follow: 'all_comments_on_watched_markets',
|
||||
answer_on_contract_you_follow: 'all_answers_on_watched_markets',
|
||||
update_on_contract_you_follow: 'market_updates_on_watched_markets',
|
||||
resolution_on_contract_you_follow: 'resolutions_on_watched_markets',
|
||||
comment_on_contract_with_users_shares_in:
|
||||
'all_comments_on_contracts_with_shares_in_on_watched_markets',
|
||||
answer_on_contract_with_users_shares_in:
|
||||
'all_answers_on_contracts_with_shares_in_on_watched_markets',
|
||||
update_on_contract_with_users_shares_in:
|
||||
'market_updates_on_watched_markets_with_shares_in',
|
||||
resolution_on_contract_with_users_shares_in:
|
||||
'resolutions_on_watched_markets_with_shares_in',
|
||||
comment_on_contract_with_users_answer: 'all_comments_on_watched_markets',
|
||||
update_on_contract_with_users_answer: 'market_updates_on_watched_markets',
|
||||
resolution_on_contract_with_users_answer: 'resolutions_on_watched_markets',
|
||||
answer_on_contract_with_users_answer: 'all_answers_on_watched_markets',
|
||||
comment_on_contract_with_users_comment: 'all_comments_on_watched_markets',
|
||||
answer_on_contract_with_users_comment: 'all_answers_on_watched_markets',
|
||||
update_on_contract_with_users_comment: 'market_updates_on_watched_markets',
|
||||
resolution_on_contract_with_users_comment: 'resolutions_on_watched_markets',
|
||||
reply_to_users_answer: 'all_replies_to_my_answers_on_watched_markets',
|
||||
reply_to_users_comment: 'all_replies_to_my_comments_on_watched_markets',
|
||||
}
|
||||
|
||||
export const getNotificationDestinationsForUser = (
|
||||
privateUser: PrivateUser,
|
||||
// TODO: accept reasons array from most to least important and work backwards
|
||||
reason: notification_reason_types | notification_preference
|
||||
) => {
|
||||
const notificationSettings = privateUser.notificationPreferences
|
||||
const unsubscribeEndpoint = getFunctionUrl('unsubscribe')
|
||||
try {
|
||||
let destinations
|
||||
let subscriptionType: notification_preference | undefined
|
||||
if (Object.keys(notificationSettings).includes(reason)) {
|
||||
subscriptionType = reason as notification_preference
|
||||
destinations = notificationSettings[subscriptionType]
|
||||
} else {
|
||||
const key = reason as notification_reason_types
|
||||
subscriptionType = notificationReasonToSubscriptionType[key]
|
||||
destinations = subscriptionType
|
||||
? notificationSettings[subscriptionType]
|
||||
: []
|
||||
}
|
||||
const optOutOfAllSettings = notificationSettings['opt_out_all']
|
||||
// Your market closure notifications are high priority, opt-out doesn't affect their delivery
|
||||
const optedOutOfEmail =
|
||||
optOutOfAllSettings.includes('email') &&
|
||||
subscriptionType !== 'your_contract_closed'
|
||||
const optedOutOfBrowser =
|
||||
optOutOfAllSettings.includes('browser') &&
|
||||
subscriptionType !== 'your_contract_closed'
|
||||
return {
|
||||
sendToEmail: destinations.includes('email') && !optedOutOfEmail,
|
||||
sendToBrowser: destinations.includes('browser') && !optedOutOfBrowser,
|
||||
unsubscribeUrl: `${unsubscribeEndpoint}?id=${privateUser.id}&type=${subscriptionType}`,
|
||||
urlToManageThisNotification: `${DOMAIN}/notifications?tab=settings§ion=${subscriptionType}`,
|
||||
}
|
||||
} catch (e) {
|
||||
// Fail safely
|
||||
console.log(
|
||||
`couldn't get notification destinations for type ${reason} for user ${privateUser.id}`
|
||||
)
|
||||
return {
|
||||
sendToEmail: false,
|
||||
sendToBrowser: false,
|
||||
unsubscribeUrl: '',
|
||||
urlToManageThisNotification: '',
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,3 @@
|
|||
import { notification_preferences } from './user-notification-preferences'
|
||||
import { ENV_CONFIG } from './envs/constants'
|
||||
import { MarketCreatorBadge, ProvenCorrectBadge, StreakerBadge } from './badge'
|
||||
|
||||
export type User = {
|
||||
id: string
|
||||
createdTime: number
|
||||
|
@ -12,96 +8,28 @@ export type User = {
|
|||
|
||||
// For their user page
|
||||
bio?: string
|
||||
bannerUrl?: string
|
||||
website?: string
|
||||
twitterHandle?: string
|
||||
discordHandle?: string
|
||||
|
||||
balance: number
|
||||
totalDeposits: number
|
||||
|
||||
profitCached: {
|
||||
daily: number
|
||||
weekly: number
|
||||
monthly: number
|
||||
allTime: number
|
||||
}
|
||||
|
||||
creatorVolumeCached: {
|
||||
daily: number
|
||||
weekly: number
|
||||
monthly: number
|
||||
allTime: number
|
||||
}
|
||||
|
||||
fractionResolvedCorrectly: number
|
||||
|
||||
nextLoanCached: number
|
||||
followerCountCached: number
|
||||
|
||||
followedCategories?: string[]
|
||||
homeSections?: string[]
|
||||
|
||||
referredByUserId?: string
|
||||
referredByContractId?: string
|
||||
referredByGroupId?: string
|
||||
lastPingTime?: number
|
||||
shouldShowWelcome?: boolean
|
||||
lastBetTime?: number
|
||||
currentBettingStreak?: number
|
||||
hasSeenContractFollowModal?: boolean
|
||||
freeMarketsCreated?: number
|
||||
isBannedFromPosting?: boolean
|
||||
|
||||
achievements: {
|
||||
provenCorrect?: {
|
||||
badges: ProvenCorrectBadge[]
|
||||
}
|
||||
marketCreator?: {
|
||||
badges: MarketCreatorBadge[]
|
||||
}
|
||||
streaker?: {
|
||||
badges: StreakerBadge[]
|
||||
}
|
||||
}
|
||||
totalPnLCached: number
|
||||
creatorVolumeCached: number
|
||||
}
|
||||
|
||||
export const STARTING_BALANCE = 1000
|
||||
export const SUS_STARTING_BALANCE = 10 // for sus users, i.e. multiple sign ups for same person
|
||||
|
||||
export type PrivateUser = {
|
||||
id: string // same as User.id
|
||||
username: string // denormalized from User
|
||||
|
||||
email?: string
|
||||
weeklyTrendingEmailSent?: boolean
|
||||
weeklyPortfolioUpdateEmailSent?: boolean
|
||||
manaBonusEmailSent?: boolean
|
||||
unsubscribedFromResolutionEmails?: boolean
|
||||
unsubscribedFromCommentEmails?: boolean
|
||||
unsubscribedFromAnswerEmails?: boolean
|
||||
initialDeviceToken?: string
|
||||
initialIpAddress?: string
|
||||
apiKey?: string
|
||||
notificationPreferences: notification_preferences
|
||||
twitchInfo?: {
|
||||
twitchName: string
|
||||
controlToken: string
|
||||
botEnabled?: boolean
|
||||
needsRelinking?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type PortfolioMetrics = {
|
||||
investmentValue: number
|
||||
balance: number
|
||||
totalDeposits: number
|
||||
timestamp: number
|
||||
userId: string
|
||||
}
|
||||
|
||||
export const MANIFOLD_USER_USERNAME = 'ManifoldMarkets'
|
||||
export const MANIFOLD_USER_NAME = 'ManifoldMarkets'
|
||||
export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png'
|
||||
|
||||
// TODO: remove. Hardcoding the strings would be better.
|
||||
// Different views require different language.
|
||||
export const BETTOR = ENV_CONFIG.bettor ?? 'trader'
|
||||
export const BETTORS = ENV_CONFIG.bettor + 's' ?? 'traders'
|
||||
export const PRESENT_BET = ENV_CONFIG.presentBet ?? 'trade'
|
||||
export const PRESENT_BETS = ENV_CONFIG.presentBet + 's' ?? 'trades'
|
||||
export const PAST_BET = ENV_CONFIG.pastBet ?? 'trade'
|
||||
export const PAST_BETS = ENV_CONFIG.pastBet + 's' ?? 'trades'
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
export function binarySearch(
|
||||
min: number,
|
||||
max: number,
|
||||
comparator: (x: number) => number
|
||||
) {
|
||||
let mid = 0
|
||||
while (true) {
|
||||
mid = min + (max - min) / 2
|
||||
|
||||
// Break once we've reached max precision.
|
||||
if (mid === min || mid === max) break
|
||||
|
||||
const comparison = comparator(mid)
|
||||
if (comparison === 0) break
|
||||
else if (comparison > 0) {
|
||||
max = mid
|
||||
} else {
|
||||
min = mid
|
||||
}
|
||||
}
|
||||
return mid
|
||||
}
|
|
@ -1,40 +1,3 @@
|
|||
import { isEqual } from 'lodash'
|
||||
|
||||
export function filterDefined<T>(array: (T | null | undefined)[]) {
|
||||
return array.filter((item) => item !== null && item !== undefined) as T[]
|
||||
}
|
||||
|
||||
export function buildArray<T>(
|
||||
...params: (T | T[] | false | undefined | null)[]
|
||||
) {
|
||||
const array: T[] = []
|
||||
|
||||
for (const el of params) {
|
||||
if (Array.isArray(el)) {
|
||||
array.push(...el)
|
||||
} else if (el) {
|
||||
array.push(el)
|
||||
}
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
export function groupConsecutive<T, U>(xs: T[], key: (x: T) => U) {
|
||||
if (!xs.length) {
|
||||
return []
|
||||
}
|
||||
const result = []
|
||||
let curr = { key: key(xs[0]), items: [xs[0]] }
|
||||
for (const x of xs.slice(1)) {
|
||||
const k = key(x)
|
||||
if (!isEqual(key, curr.key)) {
|
||||
result.push(curr)
|
||||
curr = { key: k, items: [x] }
|
||||
} else {
|
||||
curr.items.push(x)
|
||||
}
|
||||
}
|
||||
result.push(curr)
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -7,6 +7,6 @@ export const cleanUsername = (name: string, maxLength = 25) => {
|
|||
.substring(0, maxLength)
|
||||
}
|
||||
|
||||
export const cleanDisplayName = (displayName: string, maxLength = 30) => {
|
||||
export const cleanDisplayName = (displayName: string, maxLength = 25) => {
|
||||
return displayName.replace(/\s+/g, ' ').substring(0, maxLength).trim()
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
export const interpolateColor = (color1: string, color2: string, p: number) => {
|
||||
const rgb1 = parseInt(color1.replace('#', ''), 16)
|
||||
const rgb2 = parseInt(color2.replace('#', ''), 16)
|
||||
|
||||
const [r1, g1, b1] = toArray(rgb1)
|
||||
const [r2, g2, b2] = toArray(rgb2)
|
||||
|
||||
const q = 1 - p
|
||||
const rr = Math.round(r1 * q + r2 * p)
|
||||
const rg = Math.round(g1 * q + g2 * p)
|
||||
const rb = Math.round(b1 * q + b2 * p)
|
||||
|
||||
const hexString = Number((rr << 16) + (rg << 8) + rb).toString(16)
|
||||
const hex = `#${'0'.repeat(6 - hexString.length)}${hexString}`
|
||||
return hex
|
||||
}
|
||||
|
||||
function toArray(rgb: number) {
|
||||
const r = rgb >> 16
|
||||
const g = (rgb >> 8) % 256
|
||||
const b = rgb % 256
|
||||
|
||||
return [r, g, b]
|
||||
}
|
|
@ -8,68 +8,22 @@ const formatter = new Intl.NumberFormat('en-US', {
|
|||
})
|
||||
|
||||
export function formatMoney(amount: number) {
|
||||
const newAmount =
|
||||
// handle -0 case
|
||||
Math.round(amount) === 0
|
||||
? 0
|
||||
: // Handle 499.9999999999999 case
|
||||
(amount > 0 ? Math.floor : Math.ceil)(
|
||||
amount + 0.00000000001 * Math.sign(amount)
|
||||
)
|
||||
return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '')
|
||||
}
|
||||
|
||||
export function formatMoneyWithDecimals(amount: number) {
|
||||
return ENV_CONFIG.moneyMoniker + amount.toFixed(2)
|
||||
const newAmount = Math.round(amount) === 0 ? 0 : Math.floor(amount) // handle -0 case
|
||||
return (
|
||||
ENV_CONFIG.moneyMoniker + ' ' + formatter.format(newAmount).replace('$', '')
|
||||
)
|
||||
}
|
||||
|
||||
export function formatWithCommas(amount: number) {
|
||||
return formatter.format(Math.floor(amount)).replace('$', '')
|
||||
}
|
||||
|
||||
export function manaToUSD(mana: number) {
|
||||
return (mana / 100).toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})
|
||||
}
|
||||
|
||||
export function formatPercent(zeroToOne: number) {
|
||||
// Show 1 decimal place if <2% or >98%, giving more resolution on the tails
|
||||
const decimalPlaces = zeroToOne < 0.02 || zeroToOne > 0.98 ? 1 : 0
|
||||
return (zeroToOne * 100).toFixed(decimalPlaces) + '%'
|
||||
}
|
||||
|
||||
const showPrecision = (x: number, sigfigs: number) =>
|
||||
// convert back to number for weird formatting reason
|
||||
`${Number(x.toPrecision(sigfigs))}`
|
||||
|
||||
// Eg 1234567.89 => 1.23M; 5678 => 5.68K
|
||||
export function formatLargeNumber(num: number, sigfigs = 2): string {
|
||||
const absNum = Math.abs(num)
|
||||
if (absNum < 1) return showPrecision(num, sigfigs)
|
||||
|
||||
if (absNum < 100) return showPrecision(num, 2)
|
||||
if (absNum < 1000) return showPrecision(num, 3)
|
||||
if (absNum < 10000) return showPrecision(num, 4)
|
||||
|
||||
const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
|
||||
const i = Math.floor(Math.log10(absNum) / 3)
|
||||
|
||||
const numStr = showPrecision(num / Math.pow(10, 3 * i), sigfigs)
|
||||
return `${numStr}${suffix[i] ?? ''}`
|
||||
}
|
||||
|
||||
export function shortFormatNumber(num: number): string {
|
||||
if (num < 1000) return showPrecision(num, 3)
|
||||
|
||||
const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
|
||||
const i = Math.floor(Math.log10(num) / 3)
|
||||
|
||||
const numStr = showPrecision(num / Math.pow(10, 3 * i), 2)
|
||||
return `${numStr}${suffix[i] ?? ''}`
|
||||
}
|
||||
|
||||
export function toCamelCase(words: string) {
|
||||
const camelCase = words
|
||||
.split(' ')
|
||||
|
|
|
@ -1,50 +1,6 @@
|
|||
import { sortBy, sum } from 'lodash'
|
||||
|
||||
export const logInterpolation = (min: number, max: number, value: number) => {
|
||||
if (value <= min) return 0
|
||||
if (value >= max) return 1
|
||||
|
||||
return Math.log(value - min + 1) / Math.log(max - min + 1)
|
||||
}
|
||||
|
||||
export function normpdf(x: number, mean = 0, variance = 1) {
|
||||
if (variance === 0) {
|
||||
return x === mean ? Infinity : 0
|
||||
}
|
||||
|
||||
return (
|
||||
Math.exp((-0.5 * Math.pow(x - mean, 2)) / variance) /
|
||||
Math.sqrt(TAU * variance)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const EPSILON = 0.00000001
|
||||
|
||||
export function floatingEqual(a: number, b: number, epsilon = EPSILON) {
|
||||
return Math.abs(a - b) < epsilon
|
||||
}
|
||||
|
||||
export function floatingGreaterEqual(a: number, b: number, epsilon = EPSILON) {
|
||||
return a + epsilon >= b
|
||||
}
|
||||
|
||||
export function floatingLesserEqual(a: number, b: number, epsilon = EPSILON) {
|
||||
return a - epsilon <= b
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { union } from 'lodash'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
export const removeUndefinedProps = <T extends object>(obj: T): T => {
|
||||
const newObj: any = {}
|
||||
export const removeUndefinedProps = <T>(obj: T): T => {
|
||||
let newObj: any = {}
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
for (let key of Object.keys(obj)) {
|
||||
if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key]
|
||||
}
|
||||
|
||||
|
@ -14,26 +14,12 @@ export const addObjects = <T extends { [key: string]: number }>(
|
|||
obj1: T,
|
||||
obj2: T
|
||||
) => {
|
||||
const keys = union(Object.keys(obj1), Object.keys(obj2))
|
||||
const keys = _.union(Object.keys(obj1), Object.keys(obj2))
|
||||
const newObj = {} as any
|
||||
|
||||
for (const key of keys) {
|
||||
for (let key of keys) {
|
||||
newObj[key] = (obj1[key] ?? 0) + (obj2[key] ?? 0)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,115 +1,29 @@
|
|||
import { generateText, JSONContent, Node } from '@tiptap/core'
|
||||
import { generateJSON } from '@tiptap/html'
|
||||
// Tiptap starter extensions
|
||||
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||
import { Bold } from '@tiptap/extension-bold'
|
||||
import { BulletList } from '@tiptap/extension-bullet-list'
|
||||
import { Code } from '@tiptap/extension-code'
|
||||
import { CodeBlock } from '@tiptap/extension-code-block'
|
||||
import { Document } from '@tiptap/extension-document'
|
||||
import { HardBreak } from '@tiptap/extension-hard-break'
|
||||
import { Heading } from '@tiptap/extension-heading'
|
||||
import { History } from '@tiptap/extension-history'
|
||||
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
|
||||
import { Italic } from '@tiptap/extension-italic'
|
||||
import { ListItem } from '@tiptap/extension-list-item'
|
||||
import { OrderedList } from '@tiptap/extension-ordered-list'
|
||||
import { Paragraph } from '@tiptap/extension-paragraph'
|
||||
import { Strike } from '@tiptap/extension-strike'
|
||||
import { Text } from '@tiptap/extension-text'
|
||||
// other tiptap extensions
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import { Link } from '@tiptap/extension-link'
|
||||
import { Mention } from '@tiptap/extension-mention'
|
||||
import Iframe from './tiptap-iframe'
|
||||
import TiptapTweet from './tiptap-tweet-type'
|
||||
import { find } from 'linkifyjs'
|
||||
import { uniq } from 'lodash'
|
||||
import { TiptapSpoiler } from './tiptap-spoiler'
|
||||
import { MAX_TAG_LENGTH } from '../contract'
|
||||
|
||||
/** get first url in text. like "notion.so " -> "http://notion.so"; "notion" -> null */
|
||||
export function getUrl(text: string) {
|
||||
const results = find(text, 'url')
|
||||
return results.length ? results[0].href : null
|
||||
}
|
||||
|
||||
// TODO: fuzzy matching
|
||||
export const wordIn = (word: string, corpus: string) =>
|
||||
corpus.toLocaleLowerCase().includes(word.toLocaleLowerCase())
|
||||
|
||||
const checkAgainstQuery = (query: string, corpus: string) =>
|
||||
query.split(' ').every((word) => wordIn(word, corpus))
|
||||
|
||||
export const searchInAny = (query: string, ...fields: string[]) =>
|
||||
fields.some((field) => checkAgainstQuery(query, field))
|
||||
|
||||
/** @return user ids of all \@mentions */
|
||||
export function parseMentions(data: JSONContent): string[] {
|
||||
const mentions = data.content?.flatMap(parseMentions) ?? [] //dfs
|
||||
if (data.type === 'mention' && data.attrs) {
|
||||
mentions.push(data.attrs.id as string)
|
||||
export function parseTags(text: string) {
|
||||
const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi
|
||||
const matches = (text.match(regex) || []).map((match) =>
|
||||
match.trim().substring(1).substring(0, MAX_TAG_LENGTH)
|
||||
)
|
||||
const tagSet = new Set()
|
||||
const uniqueTags: string[] = []
|
||||
// Keep casing of last tag.
|
||||
matches.reverse()
|
||||
for (const tag of matches) {
|
||||
const lowercase = tag.toLowerCase()
|
||||
if (!tagSet.has(lowercase)) {
|
||||
tagSet.add(lowercase)
|
||||
uniqueTags.push(tag)
|
||||
}
|
||||
}
|
||||
return uniq(mentions)
|
||||
uniqueTags.reverse()
|
||||
return uniqueTags
|
||||
}
|
||||
|
||||
// TODO: this is a hack to get around the fact that tiptap doesn't have a
|
||||
// way to add a node view without bundling in tsx
|
||||
function skippableComponent(name: string): Node<any, any> {
|
||||
return Node.create({
|
||||
name,
|
||||
|
||||
group: 'block',
|
||||
|
||||
content: 'inline*',
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'grid-cards-component',
|
||||
},
|
||||
]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const stringParseExts = [
|
||||
// StarterKit extensions
|
||||
Blockquote,
|
||||
Bold,
|
||||
BulletList,
|
||||
Code,
|
||||
CodeBlock,
|
||||
Document,
|
||||
HardBreak,
|
||||
Heading,
|
||||
History,
|
||||
HorizontalRule,
|
||||
Italic,
|
||||
ListItem,
|
||||
OrderedList,
|
||||
Paragraph,
|
||||
Strike,
|
||||
Text,
|
||||
// other extensions
|
||||
Link,
|
||||
Image.extend({ renderText: () => '[image]' }),
|
||||
Mention, // user @mention
|
||||
Mention.extend({ name: 'contract-mention' }), // market %mention
|
||||
Iframe.extend({
|
||||
renderText: ({ node }) =>
|
||||
'[embed]' + node.attrs.src ? `(${node.attrs.src})` : '',
|
||||
}),
|
||||
skippableComponent('gridCardsComponent'),
|
||||
skippableComponent('staticReactEmbedComponent'),
|
||||
TiptapTweet.extend({ renderText: () => '[tweet]' }),
|
||||
TiptapSpoiler.extend({ renderHTML: () => ['span', '[spoiler]', 0] }),
|
||||
]
|
||||
|
||||
export function richTextToString(text?: JSONContent) {
|
||||
if (!text) return ''
|
||||
return generateText(text, stringParseExts)
|
||||
}
|
||||
|
||||
export function htmlToRichText(html: string) {
|
||||
return generateJSON(html, stringParseExts)
|
||||
export function parseWordsAsTags(text: string) {
|
||||
const taggedText = text
|
||||
.split(/\s+/)
|
||||
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`))
|
||||
.join(' ')
|
||||
return parseTags(taggedText)
|
||||
}
|
||||
|
|
|
@ -5,8 +5,7 @@ export const randomString = (length = 12) =>
|
|||
|
||||
export function genHash(str: string) {
|
||||
// xmur3
|
||||
let h: number
|
||||
for (let i = 0, h = 1779033703 ^ str.length; i < str.length; i++) {
|
||||
for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) {
|
||||
h = Math.imul(h ^ str.charCodeAt(i), 3432918353)
|
||||
h = (h << 13) | (h >>> 19)
|
||||
}
|
||||
|
@ -29,7 +28,7 @@ export function createRNG(seed: string) {
|
|||
b >>>= 0
|
||||
c >>>= 0
|
||||
d >>>= 0
|
||||
let t = (a + b) | 0
|
||||
var t = (a + b) | 0
|
||||
a = b ^ (b >>> 9)
|
||||
b = (c + (c << 3)) | 0
|
||||
c = (c << 21) | (c >>> 11)
|
||||
|
@ -40,16 +39,11 @@ export function createRNG(seed: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export const shuffle = (array: unknown[], rand: () => number) => {
|
||||
export const shuffle = (array: any[], rand: () => number) => {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const swapIndex = Math.floor(rand() * (array.length - i))
|
||||
;[array[i], array[swapIndex]] = [array[swapIndex], array[i]]
|
||||
const temp = array[i]
|
||||
array[i] = array[swapIndex]
|
||||
array[swapIndex] = temp
|
||||
}
|
||||
}
|
||||
|
||||
export function chooseRandomSubset<T>(items: T[], count: number) {
|
||||
const fiveMinutes = 5 * 60 * 1000
|
||||
const seed = Math.round(Date.now() / fiveMinutes).toString()
|
||||
shuffle(items, createRNG(seed))
|
||||
return items.slice(0, count)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,2 @@
|
|||
export const MINUTE_MS = 60 * 1000
|
||||
export const HOUR_MS = 60 * MINUTE_MS
|
||||
export const HOUR_MS = 60 * 60 * 1000
|
||||
export const DAY_MS = 24 * HOUR_MS
|
||||
|
||||
export const sleep = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
// Adopted from https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/iframe.ts
|
||||
|
||||
import { Node } from '@tiptap/core'
|
||||
|
||||
export interface IframeOptions {
|
||||
allowFullscreen: boolean
|
||||
HTMLAttributes: {
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
iframe: {
|
||||
setIframe: (options: { src: string }) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// These classes style the outer wrapper and the inner iframe;
|
||||
// Adopted from css in https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/index.vue
|
||||
const wrapperClasses = 'relative h-auto w-full overflow-hidden'
|
||||
const iframeClasses = 'absolute top-0 left-0 h-full w-full'
|
||||
|
||||
export default Node.create<IframeOptions>({
|
||||
name: 'iframe',
|
||||
|
||||
group: 'block',
|
||||
|
||||
atom: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
allowFullscreen: true,
|
||||
HTMLAttributes: {
|
||||
class: 'iframe-wrapper' + ' ' + wrapperClasses,
|
||||
// Tailwind JIT doesn't seem to pick up `pb-[20rem]`, so we hack this in:
|
||||
style: 'padding-bottom: 20rem; ',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
},
|
||||
frameborder: {
|
||||
default: 0,
|
||||
},
|
||||
height: {
|
||||
default: 0,
|
||||
},
|
||||
allowfullscreen: {
|
||||
default: this.options.allowFullscreen,
|
||||
parseHTML: () => this.options.allowFullscreen,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'iframe' }]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
this.options.HTMLAttributes.style =
|
||||
this.options.HTMLAttributes.style +
|
||||
' height: ' +
|
||||
HTMLAttributes.height +
|
||||
';'
|
||||
return [
|
||||
'div',
|
||||
this.options.HTMLAttributes,
|
||||
[
|
||||
'iframe',
|
||||
{
|
||||
...HTMLAttributes,
|
||||
class: HTMLAttributes.class + ' ' + iframeClasses,
|
||||
},
|
||||
],
|
||||
]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setIframe:
|
||||
(options: { src: string }) =>
|
||||
({ tr, dispatch }) => {
|
||||
const { selection } = tr
|
||||
const node = this.type.create(options)
|
||||
|
||||
if (dispatch) {
|
||||
tr.replaceRangeWith(selection.from, selection.to, node)
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
|
@ -1,116 +0,0 @@
|
|||
// adapted from @n8body/tiptap-spoiler
|
||||
|
||||
import {
|
||||
Mark,
|
||||
markInputRule,
|
||||
markPasteRule,
|
||||
mergeAttributes,
|
||||
} from '@tiptap/core'
|
||||
import type { ElementType } from 'react'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
spoilerEditor: {
|
||||
setSpoiler: () => ReturnType
|
||||
toggleSpoiler: () => ReturnType
|
||||
unsetSpoiler: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SpoilerOptions = {
|
||||
HTMLAttributes: Record<string, any>
|
||||
spoilerOpenClass: string
|
||||
spoilerCloseClass?: string
|
||||
inputRegex: RegExp
|
||||
pasteRegex: RegExp
|
||||
as: ElementType
|
||||
}
|
||||
|
||||
const spoilerInputRegex = /(?:^|\s)((?:\|\|)((?:[^||]+))(?:\|\|))$/
|
||||
const spoilerPasteRegex = /(?:^|\s)((?:\|\|)((?:[^||]+))(?:\|\|))/g
|
||||
|
||||
export const TiptapSpoiler = Mark.create<SpoilerOptions>({
|
||||
name: 'spoiler',
|
||||
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
inclusive: false,
|
||||
exitable: true,
|
||||
content: 'inline*',
|
||||
|
||||
priority: 1001, // higher priority than other formatting so they go inside
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: { 'aria-label': 'spoiler' },
|
||||
spoilerOpenClass: '',
|
||||
spoilerCloseClass: undefined,
|
||||
inputRegex: spoilerInputRegex,
|
||||
pasteRegex: spoilerPasteRegex,
|
||||
as: 'span',
|
||||
editing: false,
|
||||
}
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setSpoiler:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.setMark(this.name),
|
||||
toggleSpoiler:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.toggleMark(this.name),
|
||||
unsetSpoiler:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.unsetMark(this.name),
|
||||
}
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
markInputRule({
|
||||
find: this.options.inputRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
addPasteRules() {
|
||||
return [
|
||||
markPasteRule({
|
||||
find: this.options.pasteRegex,
|
||||
type: this.type,
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'span',
|
||||
getAttrs: (node) =>
|
||||
(node as HTMLElement).ariaLabel?.toLowerCase() === 'spoiler' && null,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
const elem = document.createElement(this.options.as as string)
|
||||
|
||||
Object.entries(
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||
class: this.options.spoilerCloseClass ?? this.options.spoilerOpenClass,
|
||||
})
|
||||
).forEach(([attr, val]) => elem.setAttribute(attr, val))
|
||||
|
||||
elem.addEventListener('click', () => {
|
||||
elem.setAttribute('class', this.options.spoilerOpenClass)
|
||||
})
|
||||
|
||||
return elem
|
||||
},
|
||||
})
|
|
@ -1,37 +0,0 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core'
|
||||
|
||||
export interface TweetOptions {
|
||||
tweetId: string
|
||||
}
|
||||
|
||||
// This is a version of the Tiptap Node config without addNodeView,
|
||||
// since that would require bundling in tsx
|
||||
export const TiptapTweetNode = {
|
||||
name: 'tiptapTweet',
|
||||
group: 'block',
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
tweetId: {
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'tiptap-tweet',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML(props: { HTMLAttributes: Record<string, any> }) {
|
||||
return ['tiptap-tweet', mergeAttributes(props.HTMLAttributes)]
|
||||
},
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
export default Node.create<TweetOptions>(TiptapTweetNode)
|
5
common/util/types.ts
Normal file
5
common/util/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type FirstArgument<T> = T extends (arg1: infer U, ...args: any[]) => any
|
||||
? U
|
||||
: any
|
||||
|
||||
export type Truthy<T> = Exclude<T, undefined | null | false | 0 | ''>
|
43
dev.sh
43
dev.sh
|
@ -1,43 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
ENV=${1:-dev}
|
||||
case $ENV in
|
||||
dev)
|
||||
FIREBASE_PROJECT=dev
|
||||
NEXT_ENV=DEV ;;
|
||||
prod)
|
||||
FIREBASE_PROJECT=prod
|
||||
NEXT_ENV=PROD ;;
|
||||
localdb)
|
||||
FIREBASE_PROJECT=dev
|
||||
NEXT_ENV=DEV
|
||||
EMULATOR=true ;;
|
||||
*)
|
||||
echo "Invalid environment; must be dev, prod, or localdb."
|
||||
exit 1
|
||||
esac
|
||||
|
||||
firebase use $FIREBASE_PROJECT
|
||||
|
||||
if [ ! -z $EMULATOR ]
|
||||
then
|
||||
npx concurrently \
|
||||
-n FIRESTORE,FUNCTIONS,NEXT,TS \
|
||||
-c green,white,magenta,cyan \
|
||||
"yarn --cwd=functions localDbScript" \
|
||||
"cross-env FIRESTORE_EMULATOR_HOST=localhost:8080 yarn --cwd=functions dev" \
|
||||
"cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \
|
||||
NEXT_PUBLIC_FIREBASE_EMULATE=TRUE \
|
||||
NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \
|
||||
yarn --cwd=web serve" \
|
||||
"cross-env yarn --cwd=web ts-watch"
|
||||
else
|
||||
npx concurrently \
|
||||
-n FUNCTIONS,NEXT,TS \
|
||||
-c white,magenta,cyan \
|
||||
"yarn --cwd=functions dev" \
|
||||
"cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \
|
||||
NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \
|
||||
yarn --cwd=web serve" \
|
||||
"cross-env yarn --cwd=web ts-watch"
|
||||
fi
|
2
docs/.gitattributes
vendored
2
docs/.gitattributes
vendored
|
@ -1,2 +0,0 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
20
docs/.gitignore
vendored
20
docs/.gitignore
vendored
|
@ -1,20 +0,0 @@
|
|||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
|
@ -1,2 +0,0 @@
|
|||
.docusaurus/
|
||||
build/
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
# 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
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
# 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 don’t 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. Don’t 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)**!
|
|
@ -1,71 +0,0 @@
|
|||
---
|
||||
id: about
|
||||
slug: /
|
||||
---
|
||||
|
||||
# About Manifold Markets
|
||||
|
||||
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?**
|
||||
|
||||
**Prediction markets are a place where you can bet on the outcome of future events.**
|
||||
|
||||
Consider a question like: "Will Democrats win the 2024 US presidential election?"
|
||||
|
||||
If I think the Democrats are very likely to win, and you disagree, I might offer $70 to your $30 (with the winner taking home $100 total). This set of bets imply a 70% probability of the Democrats winning.
|
||||
|
||||
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!
|
||||
|
||||
### **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 **[https://manifold.markets/stats](https://manifold.markets/stats)**.
|
||||
|
||||
### **Why is this important?**
|
||||
|
||||
Prediction markets aggregate and reveal crucial information that would not otherwise be known. They are a bottom-up mechanism that can influence everything from politics, economics, and business, to scientific research and education.
|
||||
|
||||
Prediction markets can predict **[which research papers will replicate](https://www.pnas.org/content/112/50/15343)**; which drug is the most effective; which policy would generate the most tax revenue; which charity will be underfunded; or which startup idea is the most promising. By surfacing and quantifying our collective knowledge, we as a society become wiser.
|
||||
|
||||
### **How are markets resolved?**
|
||||
|
||||
The creator of the prediction market decides the outcome and earns a commission based on the trade volume.
|
||||
|
||||
This simple resolution mechanism has surprising benefits in allowing a diversity of views to flourish. Competition between market creators will lead to traders flocking to the creators with good judgment on market resolution.
|
||||
|
||||
What's more, when the creator is free to use their judgment, many new kinds of prediction markets can be created that are less objective or even personal. (E.g. "Will I enjoy participating in the Metaverse in 2023?")
|
||||
|
||||
<!-- ### **Can I create private markets?**
|
||||
|
||||
Soon! We're running a pilot version of Manifold for Teams - private Manifold instances where you can discuss internal topics and predict on outcomes for your organization.
|
||||
|
||||
If this sounds like something you’d want, **[join the waitlist here](https://docs.google.com/forms/d/e/1FAIpQLSfM_rxRHemCjKE6KPiYXGyP2nBSInZNKn_wc7yS1-rvlLAVnA/viewform?usp=sf_link)**! -->
|
||||
|
||||
### **Who are we?**
|
||||
|
||||
Manifold Markets is currently a team of three:
|
||||
|
||||
- James Grugett
|
||||
- Stephen Grugett
|
||||
- Austin Chen
|
||||
|
||||
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!**
|
||||
|
||||
Questions? Comments? Want to create a market? Talk to us!
|
||||
|
||||
- Email: **[info@manifold.markets](mailto:info@manifold.markets)**
|
||||
- Office hours:
|
||||
- **[Calendly — Austin](https://calendly.com/austinchen/manifold)**
|
||||
- **[Calendly — James](https://calendly.com/jamesgrugett/manifold)**
|
||||
- Chat: **[Manifold Markets Discord server](https://discord.gg/eHQBNBqXuh)**
|
||||
|
||||
## **Further Reading**
|
||||
|
||||
- **[Above the fold](https://manifoldmarkets.substack.com/)**, our newsletter
|
||||
- **[Scott Alexander on play-money prediction markets](https://astralcodexten.substack.com/p/play-money-and-reputation-systems)**
|
789
docs/docs/api.md
789
docs/docs/api.md
|
@ -1,789 +0,0 @@
|
|||
# API
|
||||
|
||||
:::caution
|
||||
|
||||
Our API is still in alpha — things may change or break at any time!
|
||||
|
||||
If you have questions, come chat with us on [Discord](https://discord.com/invite/eHQBNBqXuh). We’d love to hear about what you build!
|
||||
|
||||
:::
|
||||
|
||||
## 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/user/[username]`
|
||||
|
||||
Gets a user by their username. Remember that usernames may change.
|
||||
|
||||
Requires no authorization.
|
||||
|
||||
### `GET /v0/user/by-id/[id]`
|
||||
|
||||
Gets a user by their unique ID. Many other API endpoints return this as the `userId`.
|
||||
|
||||
Requires no authorization.
|
||||
|
||||
### GET /v0/me
|
||||
|
||||
Returns the authenticated user.
|
||||
|
||||
### `GET /v0/groups`
|
||||
|
||||
Gets all groups, in no particular order.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `availableToUserId`: Optional. if specified, only groups that the user can
|
||||
join and groups they've already joined will be returned.
|
||||
|
||||
Requires no authorization.
|
||||
|
||||
### `GET /v0/group/[slug]`
|
||||
|
||||
Gets a group by its slug.
|
||||
|
||||
Requires no authorization.
|
||||
Note: group is singular in the URL.
|
||||
|
||||
### `GET /v0/group/by-id/[id]`
|
||||
|
||||
Gets a group by its unique ID.
|
||||
|
||||
Requires no authorization.
|
||||
Note: group is singular in the URL.
|
||||
|
||||
### `GET /v0/group/by-id/[id]/markets`
|
||||
|
||||
Gets a group's markets by its unique ID.
|
||||
|
||||
Requires no authorization.
|
||||
Note: group is singular in the URL.
|
||||
|
||||
### `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
|
||||
```
|
||||
https://manifold.markets/api/v0/markets?limit=1
|
||||
```
|
||||
- Example response
|
||||
```json
|
||||
[
|
||||
{
|
||||
"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?",
|
||||
"tags":[
|
||||
"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`
|
||||
|
||||
```tsx
|
||||
// Information about a market, but without bets or comments
|
||||
type LiteMarket = {
|
||||
// Unique identifer for this market
|
||||
id: string
|
||||
|
||||
// Attributes about the creator
|
||||
creatorUsername: string
|
||||
creatorName: string
|
||||
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
|
||||
|
||||
// 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
|
||||
|
||||
outcomeType: string // BINARY, FREE_RESPONSE, MULTIPLE_CHOICE, NUMERIC, or PSEUDO_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
|
||||
min?: number // PSEUDO_NUMERIC markets only, the minimum resolvable value
|
||||
max?: number // PSEUDO_NUMERIC markets only, the maximum resolvable value
|
||||
isLogScale?: bool // PSEUDO_NUMERIC markets only, if true `number = (max - min + 1)^probability + minstart - 1`, otherwise `number = min + (max - min) * probability`
|
||||
|
||||
volume: number
|
||||
volume7Days: number
|
||||
volume24Hours: number
|
||||
|
||||
isResolved: boolean
|
||||
resolutionTime?: number
|
||||
resolution?: string
|
||||
resolutionProbability?: number // Used for BINARY markets resolved to MKT
|
||||
|
||||
lastUpdatedTime?: number
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /v0/market/[marketId]`
|
||||
|
||||
Gets information about a single market by ID. Includes comments, bets, and answers.
|
||||
|
||||
Requires no authorization.
|
||||
|
||||
- Example request
|
||||
|
||||
```
|
||||
https://manifold.markets/api/v0/market/3zspH9sSzMlbFQLn9GKR
|
||||
```
|
||||
|
||||
- <details><summary>Example response</summary><p>
|
||||
|
||||
```json
|
||||
{
|
||||
"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 I’ll 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": true,
|
||||
"resolution": "MKT",
|
||||
"resolutionTime": 1655265001448,
|
||||
"answers": [
|
||||
{
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
{
|
||||
"number": 4,
|
||||
"name": "James Grugett",
|
||||
"userId": "5LZ4LgYuySdL1huCWe7bti02ghx2",
|
||||
"text": "Utility (Defined by your personal utility function.)",
|
||||
"createdTime": 1655264793224,
|
||||
"contractId": "lEoqtnDgJzft6apSKzYK",
|
||||
"username": "JamesGrugett",
|
||||
"id": "4",
|
||||
"avatarUrl": "https://lh3.googleusercontent.com/a-/AOh14GjC83uMe-fEfzd6QvxiK6ZqZdlMytuHxevgMYIkpAI=s96-c",
|
||||
"probability": 0.09211463154147384
|
||||
}
|
||||
],
|
||||
"comments": [
|
||||
{
|
||||
"id": "ZdHIyfQazHyl8nI0ENS7",
|
||||
"userId": "qe2QqIlOkeWsbljfeF3MsxpSJ9i2",
|
||||
"createdTime": 1655265807433,
|
||||
"text": "ok what\ni did not resolve this intentionally",
|
||||
"contractId": "lEoqtnDgJzft6apSKzYK",
|
||||
"userName": "Angela",
|
||||
"userAvatarUrl": "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",
|
||||
"userUsername": "Angela"
|
||||
},
|
||||
{
|
||||
"userName": "James Grugett",
|
||||
"userUsername": "JamesGrugett",
|
||||
"id": "F7fvHGhTiFal8uTsUc9P",
|
||||
"userAvatarUrl": "https://lh3.googleusercontent.com/a-/AOh14GjC83uMe-fEfzd6QvxiK6ZqZdlMytuHxevgMYIkpAI=s96-c",
|
||||
"replyToCommentId": "ZdHIyfQazHyl8nI0ENS7",
|
||||
"text": "@Angela Sorry! There was an error that automatically resolved several markets that were created in the last few hours.",
|
||||
"createdTime": 1655266286514,
|
||||
"userId": "5LZ4LgYuySdL1huCWe7bti02ghx2",
|
||||
"contractId": "lEoqtnDgJzft6apSKzYK"
|
||||
},
|
||||
{
|
||||
"userId": "qe2QqIlOkeWsbljfeF3MsxpSJ9i2",
|
||||
"contractId": "lEoqtnDgJzft6apSKzYK",
|
||||
"id": "PIHhXy5hLHSgW8uoUD0Q",
|
||||
"userName": "Angela",
|
||||
"text": "lmk if anyone lost manna from this situation and i'll try to fix it",
|
||||
"userUsername": "Angela",
|
||||
"createdTime": 1655277581308,
|
||||
"userAvatarUrl": "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"
|
||||
},
|
||||
{
|
||||
"userAvatarUrl": "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",
|
||||
"userName": "Angela",
|
||||
"text": "from my end it looks like no one did",
|
||||
"replyToCommentId": "PIHhXy5hLHSgW8uoUD0Q",
|
||||
"createdTime": 1655287149528,
|
||||
"userUsername": "Angela",
|
||||
"id": "5slnWEQWwm6dHjDi6oiH",
|
||||
"contractId": "lEoqtnDgJzft6apSKzYK",
|
||||
"userId": "qe2QqIlOkeWsbljfeF3MsxpSJ9i2"
|
||||
}
|
||||
],
|
||||
"bets": [
|
||||
{
|
||||
"outcome": "0",
|
||||
"contractId": "lEoqtnDgJzft6apSKzYK",
|
||||
"fees": {
|
||||
"liquidityFee": 0,
|
||||
"creatorFee": 0,
|
||||
"platformFee": 0
|
||||
},
|
||||
"isAnte": true,
|
||||
"shares": 100,
|
||||
"probAfter": 1,
|
||||
"amount": 100,
|
||||
"userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
|
||||
"createdTime": 1655258914863,
|
||||
"probBefore": 0,
|
||||
"id": "2jNZqnwoEQL7WDTTAWDP"
|
||||
},
|
||||
{
|
||||
"shares": 173.20508075688772,
|
||||
"fees": {
|
||||
"platformFee": 0,
|
||||
"liquidityFee": 0,
|
||||
"creatorFee": 0
|
||||
},
|
||||
"contractId": "lEoqtnDgJzft6apSKzYK",
|
||||
"probBefore": 0,
|
||||
"createdTime": 1655258941573,
|
||||
"loanAmount": 0,
|
||||
"userId": "qe2QqIlOkeWsbljfeF3MsxpSJ9i2",
|
||||
"amount": 100,
|
||||
"outcome": "1",
|
||||
"probAfter": 0.75,
|
||||
"id": "xuc3JoiNkE8lXPh15mUb"
|
||||
},
|
||||
{
|
||||
"userId": "y1hb6k7txdZPV5mgyxPFApZ7nQl2",
|
||||
"contractId": "lEoqtnDgJzft6apSKzYK",
|
||||
"loanAmount": 0,
|
||||
"probAfter": 0.009925496893641248,
|
||||
"id": "8TBlzPtOdO0q5BgSyRbi",
|
||||
"createdTime": 1655261198074,
|
||||
"shares": 20.024984394500787,
|
||||
"amount": 1,
|
||||
"outcome": "2",
|
||||
"probBefore": 0,
|
||||
"fees": {
|
||||
"liquidityFee": 0,
|
||||
"creatorFee": 0,
|
||||
"platformFee": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"probAfter": 0.00987648269777473,
|
||||
"outcome": "3",
|
||||
"id": "9vdwes6s9QxbYZUBhHs4",
|
||||
"createdTime": 1655263226587,
|
||||
"shares": 20.074859899884732,
|
||||
"amount": 1,
|
||||
"loanAmount": 0,
|
||||
"fees": {
|
||||
"liquidityFee": 0,
|
||||
"platformFee": 0,
|
||||
"creatorFee": 0
|
||||
},
|
||||
"userId": "jbgplxty4kUKIa1MmgZk22byJq03",
|
||||
"contractId": "lEoqtnDgJzft6apSKzYK",
|
||||
"probBefore": 0
|
||||
},
|
||||
{
|
||||
"createdTime": 1655264793224,
|
||||
"fees": {
|
||||
"creatorFee": 0,
|
||||
"liquidityFee": 0,
|
||||
"platformFee": 0
|
||||
},
|
||||
"probAfter": 0.09211463154147384,
|
||||
"amount": 10,
|
||||
"id": "BehiSGgk1wAkIWz1a8L4",
|
||||
"userId": "5LZ4LgYuySdL1huCWe7bti02ghx2",
|
||||
"contractId": "lEoqtnDgJzft6apSKzYK",
|
||||
"loanAmount": 0,
|
||||
"probBefore": 0,
|
||||
"outcome": "4",
|
||||
"shares": 64.34283176858165
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
- Response type: A `FullMarket`
|
||||
|
||||
```tsx
|
||||
// A complete market, along with bets, comments, and answers (for free response markets)
|
||||
type FullMarket = LiteMarket & {
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
answers?: Answer[] // dpm-2 markets only
|
||||
description: JSONContent // Rich text content. See https://tiptap.dev/guide/output#option-1-json
|
||||
textDescription: string // string description without formatting, images, or embeds
|
||||
}
|
||||
|
||||
type Bet = {
|
||||
id: string
|
||||
contractId: string
|
||||
|
||||
amount: number // bet size; negative if SELL bet
|
||||
outcome: string
|
||||
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
|
||||
isAnte?: boolean
|
||||
|
||||
createdTime: number
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /v0/slug/[marketSlug]`
|
||||
|
||||
Gets information about a single market by slug (the portion of the URL path after the username).
|
||||
|
||||
Requires no authorization.
|
||||
|
||||
- Example request
|
||||
```
|
||||
https://manifold.markets/api/v0/slug/will-carrick-flynn-win-the-general
|
||||
```
|
||||
- Response type: A `FullMarket` ; same as above.
|
||||
|
||||
### `GET /v0/users`
|
||||
|
||||
Lists all users.
|
||||
|
||||
Requires no authorization.
|
||||
|
||||
- Example request
|
||||
```
|
||||
https://manifold.markets/api/v0/users
|
||||
```
|
||||
- Example response
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id":"igi2zGXsfxYPgB0DJTXVJVmwCOr2",
|
||||
"createdTime":1639011767273,
|
||||
"name":"Austin",
|
||||
"username":"Austin",
|
||||
"url":"https://manifold.markets/Austin",
|
||||
"avatarUrl":"https://lh3.googleusercontent.com/a-/AOh14GiZyl1lBehuBMGyJYJhZd-N-mstaUtgE4xdI22lLw=s96-c",
|
||||
"bio":"I build Manifold! Always happy to chat; reach out on Discord or find a time on https://calendly.com/austinchen/manifold!",
|
||||
"bannerUrl":"https://images.unsplash.com/photo-1501523460185-2aa5d2a0f981?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1531&q=80",
|
||||
"website":"https://blog.austn.io",
|
||||
"twitterHandle":"akrolsmir",
|
||||
"discordHandle":"akrolsmir#4125",
|
||||
"balance":9122.607163564959,
|
||||
"totalDeposits":10339.004780544328,
|
||||
"totalPnLCached":9376.601262721899,
|
||||
"creatorVolumeCached":76078.46984199001
|
||||
}
|
||||
```
|
||||
- Response type: Array of `LiteUser`
|
||||
|
||||
```tsx
|
||||
// Basic information about a user
|
||||
type LiteUser = {
|
||||
id: string // user's unique id
|
||||
createdTime: number
|
||||
|
||||
name: string // display name, may contain spaces
|
||||
username: string // username, used in urls
|
||||
url: string // link to user's profile
|
||||
avatarUrl?: string
|
||||
|
||||
bio?: string
|
||||
bannerUrl?: string
|
||||
website?: string
|
||||
twitterHandle?: string
|
||||
discordHandle?: string
|
||||
|
||||
// Note: the following are here for convenience only and may be removed in the future.
|
||||
balance: number
|
||||
totalDeposits: number
|
||||
totalPnLCached: number
|
||||
creatorVolumeCached: number
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /v0/bet`
|
||||
|
||||
Places a new bet on behalf of the authorized user.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `amount`: Required. The amount to bet, in M$, before fees.
|
||||
- `contractId`: Required. The ID of the contract (market) to bet on.
|
||||
- `outcome`: Required. The outcome to bet on. For binary markets, this is `YES`
|
||||
or `NO`. For free response markets, this is the ID of the free response
|
||||
answer. For numeric markets, this is a string representing the target bucket,
|
||||
and an additional `value` parameter is required which is a number representing
|
||||
the target value. (Bet on numeric markets at your own peril.)
|
||||
- `limitProb`: Optional. A number between `0.001` and `0.999` inclusive representing
|
||||
the limit probability for your bet (i.e. 0.1% to 99.9% — multiply by 100 for the
|
||||
probability percentage).
|
||||
The bet will execute immediately in the direction of `outcome`, but not beyond this
|
||||
specified limit. If not all the bet is filled, the bet will remain as an open offer
|
||||
that can later be matched against an opposite direction bet.
|
||||
- For example, if the current market probability is `50%`:
|
||||
- A `M$10` bet on `YES` with `limitProb=0.4` would not be filled until the market
|
||||
probability moves down to `40%` and someone bets `M$15` of `NO` to match your
|
||||
bet odds.
|
||||
- A `M$100` bet on `YES` with `limitProb=0.6` would fill partially or completely
|
||||
depending on current unfilled limit bets and the AMM's liquidity. Any remaining
|
||||
portion of the bet not filled would remain to be matched against in the future.
|
||||
- An unfilled limit order bet can be cancelled using the cancel API.
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
$ curl https://manifold.markets/api/v0/bet -X POST -H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Key {...}' \
|
||||
--data-raw '{"amount":1, \
|
||||
"outcome":"YES", \
|
||||
"contractId":"{...}"}'
|
||||
```
|
||||
|
||||
### `POST /v0/bet/cancel/[id]`
|
||||
|
||||
Cancel the limit order of a bet with the specified id. If the bet was unfilled, it will be cancelled so that no other bets will match with it. This is action irreversable.
|
||||
|
||||
### `POST /v0/market`
|
||||
|
||||
Creates a new market on behalf of the authorized user.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `outcomeType`: Required. One of `BINARY`, `FREE_RESPONSE`, `MULTIPLE_CHOICE`, or `PSEUDO_NUMERIC`.
|
||||
- `question`: Required. The headline question for the market.
|
||||
- `description`: Required. A long description describing the rules for the market.
|
||||
- Note: string descriptions do **not** turn into links, mentions, formatted text. Instead, rich text descriptions must be in [TipTap json](https://tiptap.dev/guide/output#option-1-json).
|
||||
- `closeTime`: Required. The time at which the market will close, represented as milliseconds since the epoch.
|
||||
- `tags`: Optional. An array of string tags for the market.
|
||||
|
||||
For binary markets, you must also provide:
|
||||
|
||||
- `initialProb`: An initial probability for the market, between 1 and 99.
|
||||
|
||||
For numeric markets, you must also provide:
|
||||
|
||||
- `min`: The minimum value that the market may resolve to.
|
||||
- `max`: The maximum value that the market may resolve to.
|
||||
- `isLogScale`: If true, your numeric market will increase exponentially from min to max.
|
||||
- `initialValue`: An initial value for the market, between min and max, exclusive.
|
||||
|
||||
For multiple choice markets, you must also provide:
|
||||
|
||||
- `answers`: An array of strings, each of which will be a valid answer for the market.
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
$ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Key {...}'
|
||||
--data-raw '{"outcomeType":"BINARY", \
|
||||
"question":"Is there life on Mars?", \
|
||||
"description":"I'm not going to type some long ass example description.", \
|
||||
"closeTime":1700000000000, \
|
||||
"initialProb":25}'
|
||||
```
|
||||
|
||||
### `POST /v0/market/[marketId]/add-liquidity`
|
||||
|
||||
Adds a specified amount of liquidity into the market.
|
||||
|
||||
- `amount`: Required. The amount of liquidity to add, in M$.
|
||||
|
||||
### `POST /v0/market/[marketId]/close`
|
||||
|
||||
Closes a market on behalf of the authorized user.
|
||||
|
||||
- `closeTime`: Optional. Milliseconds since the epoch to close the market at. If not provided, the market will be closed immediately. Cannot provide close time in past.
|
||||
|
||||
### `POST /v0/market/[marketId]/resolve`
|
||||
|
||||
Resolves a market on behalf of the authorized user.
|
||||
|
||||
Parameters:
|
||||
|
||||
For binary markets:
|
||||
|
||||
- `outcome`: Required. One of `YES`, `NO`, `MKT`, or `CANCEL`.
|
||||
- `probabilityInt`: Optional. The probability to use for `MKT` resolution.
|
||||
|
||||
For free response or multiple choice markets:
|
||||
|
||||
- `outcome`: Required. One of `MKT`, `CANCEL`, or a `number` indicating the answer index.
|
||||
- `resolutions`: An array of `{ answer, pct }` objects to use as the weights for resolving in favor of multiple free response options. Can only be set with `MKT` outcome. Note that the total weights must add to 100.
|
||||
|
||||
For numeric markets:
|
||||
|
||||
- `outcome`: Required. One of `CANCEL`, or a `number` indicating the selected numeric bucket ID.
|
||||
- `value`: The value that the market may resolves to.
|
||||
- `probabilityInt`: Required if `value` is present. Should be equal to
|
||||
- If log scale: `log10(value - min + 1) / log10(max - min + 1)`
|
||||
- Otherwise: `(value - min) / (max - min)`
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
# Resolve a binary market
|
||||
$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Key {...}' \
|
||||
--data-raw '{"outcome": "YES"}'
|
||||
|
||||
# Resolve a binary market with a specified probability
|
||||
$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Key {...}' \
|
||||
--data-raw '{"outcome": "MKT", \
|
||||
"probabilityInt": 75}'
|
||||
|
||||
# Resolve a free response market with a single answer chosen
|
||||
$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Key {...}' \
|
||||
--data-raw '{"outcome": 2}'
|
||||
|
||||
# Resolve a free response market with multiple answers chosen
|
||||
$ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Key {...}' \
|
||||
--data-raw '{"outcome": "MKT", \
|
||||
"resolutions": [ \
|
||||
{"answer": 0, "pct": 50}, \
|
||||
{"answer": 2, "pct": 50} \
|
||||
]}'
|
||||
```
|
||||
|
||||
### `POST /v0/market/[marketId]/sell`
|
||||
|
||||
Sells some quantity of shares in a binary market on behalf of the authorized user.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `outcome`: Optional. One of `YES`, or `NO`. If you leave it off, and you only
|
||||
own one kind of shares, you will sell that kind of shares.
|
||||
- `shares`: Optional. The amount of shares to sell of the outcome given
|
||||
above. If not provided, all the shares you own will be sold.
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
$ curl https://manifold.markets/api/v0/market/{marketId}/sell -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Key {...}' \
|
||||
--data-raw '{"outcome": "YES", "shares": 10}'
|
||||
```
|
||||
|
||||
### `POST /v0/comment`
|
||||
|
||||
Creates a comment in the specified market. Only supports top-level comments for now.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `contractId`: Required. The ID of the market to comment on.
|
||||
- `content`: The comment to post, formatted as [TipTap json](https://tiptap.dev/guide/output#option-1-json), OR
|
||||
- `html`: The comment to post, formatted as an HTML string, OR
|
||||
- `markdown`: The comment to post, formatted as a markdown string.
|
||||
|
||||
### `GET /v0/bets`
|
||||
|
||||
Gets a list of bets, ordered by creation date descending.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `username`: Optional. If set, the response will include only bets created by this user.
|
||||
- `market`: Optional. The slug of a market. If set, the response will only include bets on this market.
|
||||
- `limit`: Optional. How many bets to return. The maximum and the default is 1000.
|
||||
- `before`: Optional. The ID of the bet before which the list will start. For
|
||||
example, if you ask for the most recent 10 bets, and then perform a second
|
||||
query for 10 more bets with `before=[the id of the 10th bet]`, you will
|
||||
get bets 11 through 20.
|
||||
|
||||
Requires no authorization.
|
||||
|
||||
- Example request
|
||||
```
|
||||
https://manifold.markets/api/v0/bets?username=ManifoldMarkets&market=will-i-be-able-to-place-a-limit-ord
|
||||
```
|
||||
- Response type: A `Bet[]`.
|
||||
|
||||
- <details><summary>Example response</summary><p>
|
||||
|
||||
```json
|
||||
[
|
||||
// Limit bet, partially filled.
|
||||
{
|
||||
"isFilled": false,
|
||||
"amount": 15.596681605353808,
|
||||
"userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
|
||||
"contractId": "Tz5dA01GkK5QKiQfZeDL",
|
||||
"probBefore": 0.5730753474948571,
|
||||
"isCancelled": false,
|
||||
"outcome": "YES",
|
||||
"fees": { "creatorFee": 0, "liquidityFee": 0, "platformFee": 0 },
|
||||
"shares": 31.193363210707616,
|
||||
"limitProb": 0.5,
|
||||
"id": "yXB8lVbs86TKkhWA1FVi",
|
||||
"loanAmount": 0,
|
||||
"orderAmount": 100,
|
||||
"probAfter": 0.5730753474948571,
|
||||
"createdTime": 1659482775970,
|
||||
"fills": [
|
||||
{
|
||||
"timestamp": 1659483249648,
|
||||
"matchedBetId": "MfrMd5HTiGASDXzqibr7",
|
||||
"amount": 15.596681605353808,
|
||||
"shares": 31.193363210707616
|
||||
}
|
||||
]
|
||||
},
|
||||
// Normal bet (no limitProb specified).
|
||||
{
|
||||
"shares": 17.350459904608414,
|
||||
"probBefore": 0.5304358279113885,
|
||||
"isFilled": true,
|
||||
"probAfter": 0.5730753474948571,
|
||||
"userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
|
||||
"amount": 10,
|
||||
"contractId": "Tz5dA01GkK5QKiQfZeDL",
|
||||
"id": "1LPJHNz5oAX4K6YtJlP1",
|
||||
"fees": {
|
||||
"platformFee": 0,
|
||||
"liquidityFee": 0,
|
||||
"creatorFee": 0.4251333951457593
|
||||
},
|
||||
"isCancelled": false,
|
||||
"loanAmount": 0,
|
||||
"orderAmount": 10,
|
||||
"fills": [
|
||||
{
|
||||
"amount": 10,
|
||||
"matchedBetId": null,
|
||||
"shares": 17.350459904608414,
|
||||
"timestamp": 1659482757271
|
||||
}
|
||||
],
|
||||
"createdTime": 1659482757271,
|
||||
"outcome": "YES"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
## Changelog
|
||||
|
||||
- 2022-09-24: Expand market POST docs to include new market types (`PSEUDO_NUMERIC`, `MULTIPLE_CHOICE`)
|
||||
- 2022-07-15: Add user by username and user by ID APIs
|
||||
- 2022-06-08: Add paging to markets endpoint
|
||||
- 2022-06-05: Add new authorized write endpoints
|
||||
- 2022-02-28: Add `resolutionTime` to markets, change `closeTime` definition
|
||||
- 2022-02-19: Removed user IDs from bets
|
||||
- 2022-02-17: Released our v0 API, with `/markets`, `/market/[marketId]`, and `/slug/[slugId]`
|
|
@ -1,47 +0,0 @@
|
|||
# Awesome Manifold 😎
|
||||
|
||||
A list of community-created projects built on, or related to, Manifold Markets.
|
||||
|
||||
## Data
|
||||
|
||||
- [Manifold Market Stats](https://wasabipesto.com/jupyter/manifold/)
|
||||
|
||||
## Sites using Manifold
|
||||
|
||||
- [WagerWith.me](https://www.wagerwith.me/) — Bet with your friends, with full Manifold integration to bet with M$.
|
||||
- [Alignment Markets](https://alignmentmarkets.com/) - Bet on the progress of benchmarks in ML safety!
|
||||
|
||||
## API / Dev
|
||||
|
||||
- [PyManifold](https://github.com/bcongdon/PyManifold) - Python client for the Manifold API
|
||||
- [PyManifold fork](https://github.com/gappleto97/PyManifold/) - Fork maintained by [@LivInTheLookingGlass](https://manifold.markets/LivInTheLookingGlass)
|
||||
- [manifold-markets-python](https://github.com/vluzko/manifold-markets-python) - Python tools for working with Manifold Markets (including various accuracy metrics)
|
||||
- [ManifoldMarketManager](https://github.com/gappleto97/ManifoldMarketManager) - Python script and library to automatically manage markets
|
||||
- [manifeed](https://github.com/joy-void-joy/manifeed) - Tool that creates an RSS feed for new Manifold markets
|
||||
- [manifold-sdk](https://github.com/keriwarr/manifold-sdk) - TypeScript/JavaScript client for the Manifold API
|
||||
|
||||
## Bots
|
||||
|
||||
- [@manifold@botsin.space](https://botsin.space/@manifold) - Posts new Manifold markets to Mastodon
|
||||
- [James' Bot](https://github.com/manifoldmarkets/market-maker) — Simple trading bot that makes markets
|
||||
- [mana](https://github.com/AnnikaCodes/mana) - A Discord bot for Manifold by [@arae](https://manifold.markets/arae)
|
||||
|
||||
## Writeups
|
||||
|
||||
- [Information Markets, Decision Markets, Attention Markets, Action Markets](https://astralcodexten.substack.com/p/information-markets-decision-markets) by Scott Alexander
|
||||
- [Mismatched Monetary Motivation in Manifold Markets](https://kevin.zielnicki.com/2022/02/17/manifold/) by Kevin Zielnicki
|
||||
- [Introducing the Salem/CSPI Forecasting Tournament](https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting) by Richard Hanania
|
||||
- [What I learned about running a betting market game night contest](https://shakeddown.wordpress.com/2022/08/04/what-i-learned-about-running-a-betting-market-game-night-contest/) by shakeddown
|
||||
- [Free-riding on prediction markets](https://pedunculate.substack.com/p/free-riding-on-prediction-markets) by John Roxton
|
||||
|
||||
## Art
|
||||
|
||||
- Folded origami and doodles by [@hamnox](https://manifold.markets/hamnox) ![](https://i.imgur.com/nVGY4pL.png)
|
||||
- Laser-cut Foldy by [@wasabipesto](https://manifold.markets/wasabipesto) ![](https://i.imgur.com/g9S6v3P.jpg)
|
||||
|
||||
## Alumni
|
||||
|
||||
_These projects are no longer active, but were really really cool!_
|
||||
|
||||
- [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold
|
||||
- [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government
|
|
@ -1,145 +0,0 @@
|
|||
# Bounties
|
||||
|
||||
## What are Manifold bounties?
|
||||
|
||||
From time to time, a member of our community goes above and beyond in helping Manifold make prediction markets accessible & ubiquitous. We’d like to recognize such contributions publicly, and include a token of our appreciation in the form of M$!
|
||||
|
||||
Examples of community work that may be eligible for a bounty:
|
||||
|
||||
- Blog posts, markets, or comments which lead us to significantly change our views
|
||||
- A track record of creating markets that help people make better decisions
|
||||
- Promoting Manifold & forecasting to a wider audience
|
||||
- Identifying serious exploits with our financial infrastructure
|
||||
|
||||
Our community is the beating heart of Manifold; your individual contributions are what make this platform valuable at all. Thanks to everyone listed here (as well as countless unnamed others) for your help & support!
|
||||
|
||||
## Awarded bounties
|
||||
|
||||
💥 *Awarded on 2022-10-07*
|
||||
|
||||
**[Pepe](https://manifold.markets/Pepe): M$10,000**
|
||||
**[Jack](https://manifold.markets/jack): M$2,000**
|
||||
**[Martin](https://manifold.markets/MartinRandall): M$2,000**
|
||||
**[Yev](https://manifold.markets/Yev): M$2,000**
|
||||
**[Michael](https://manifold.markets/MichaelWheatley): M$2,000**
|
||||
|
||||
- For discovering an infinite mana exploit using limit orders, and informing the Manifold team of it privately.
|
||||
|
||||
**[Matt](https://manifold.markets/MattP): M$5,000**
|
||||
**[Adrian](https://manifold.markets/ahalekelly): M$5,000**
|
||||
**[Yev](https://manifold.markets/Yev): M$5,000**
|
||||
|
||||
- For discovering an AMM liquidity exploit and informing the Manifold team of it privately.
|
||||
|
||||
🎈 *Awarded on 2022-06-14*
|
||||
|
||||
**[Wasabipesto](https://manifold.markets/wasabipesto): M$20,000**
|
||||
|
||||
- For creating an awesome stats page which features and analyses various data sets! This can be found on the second tab of our [analytics page](https://manifold.markets/stats).
|
||||
|
||||
**[Jack](https://manifold.markets/jack): M$10,000**
|
||||
|
||||
- For adding a bunch of charities to [Manifold for Good](https://manifold.markets/charity), working out market math with Austin, and excellent comment activity.
|
||||
|
||||
**[Forrest](https://manifold.markets/Forrest): M$10,000**
|
||||
|
||||
- For a variety of [open source code contributions](https://github.com/manifoldmarkets/manifold/commits?author=ForrestWeiswolf), making our code base easier to use and maintain.
|
||||
|
||||
**[IsaacKing](https://manifold.markets/IsaacKing): M$10,000**
|
||||
|
||||
- For responsible disclosure of an exploit involving liquidity withdrawal, which has [now been fixed](https://github.com/manifoldmarkets/manifold/pull/472)! Removing one infinite money glitch at a time.
|
||||
|
||||
**[Sjlver](https://manifold.markets/Sjlver): M$5,000**
|
||||
|
||||
- For responsible disclosure of a potential exploit. We would say what it is, but it isn’t quite fixed yet! 🤫
|
||||
|
||||
_🌿 Announced on 2022-05-02_
|
||||
|
||||
**[Marshall Polaris](https://manifold.markets/mqp): M$200K**
|
||||
|
||||
- For spearheading the effort to [open-source Manifold](https://github.com/manifoldmarkets/manifold), by documenting our processes, triaging bugs, and improving the new contributor experience.
|
||||
- Marshall contributed over 2 weeks of part-time volunteer work; as such, we are awarding an amount that reflects the extraordinary amount of effort he’s put in.
|
||||
|
||||
**[Vincent Luczkow](https://manifold.markets/VincentLuczkow): M$10,000**
|
||||
|
||||
- For building and releasing https://github.com/vluzko/manifold-markets-python, a super cool Python visualization of the calibration accuracy of all Manifold markets. Turns out we’re doing okay!
|
||||
|
||||
**[Akhil Wable](https://manifold.markets/AkhilWable): M$10,000**
|
||||
|
||||
- For writing up [Akhil’s Product Suggestions](https://www.notion.so/Akhil-s-Product-Suggestions-672e1cba393d4242852ff95ae79528df), an extensive, thoughtful list of improvements we could make to our platform.
|
||||
|
||||
**[Alex K. Chen](https://manifold.markets/AlexKChen): M$6,000**
|
||||
|
||||
- For the creation of a metric ton of innovative, long term questions. At the time of award, Alex was singlehandedly responsible for 20% of all markets posted in April.
|
||||
|
||||
**[ZorbaTHut](https://manifold.markets/ZorbaTHut): M$5,000**
|
||||
|
||||
- For [testing out futarchy](https://manifold.markets/tag/themotte_leaving) on an important problem for the community of The Motte.
|
||||
|
||||
**[Tetraspace](https://manifold.markets/Tetraspace): M$3,500**
|
||||
|
||||
- For the creation of [a focused set of questions on UK politics](https://twitter.com/TetraspaceWest/status/1516824123149848579), with relevant real-world predictions.
|
||||
- For the idea and execution of using FR bounded buckets for mapping out a scalar range ([example market](https://manifold.markets/Tetraspace/if-ron-desantis-is-elected-presiden), [discussion here](https://manifold.markets/StephenMalina/how-many-daily-active-users-will-ma)).
|
||||
|
||||
**[tcheasdfjkl](https://manifold.markets/tcheasdfjkl): M$2,500**
|
||||
|
||||
- For calling out numerous areas of improvement, e.g. around our profit numbers being wonky, and problems with the DPM ⇒ CFMM market conversions.
|
||||
|
||||
**[Jack](https://manifold.markets/JackC): M$500**
|
||||
|
||||
- For recommending we list the Long-Term Future Fund as a supported charity.
|
||||
|
||||
**[N.C. Young](https://manifold.markets/NcyRocks): M$500**
|
||||
|
||||
- For recommending we list the Givewell Maximum Impact Fund as a supported charity.
|
||||
|
||||
\**🥧 *Awarded 2022-03-14\*
|
||||
|
||||
**[Kevin Zielnicki](https://manifold.markets/kjz): M$10,000**
|
||||
|
||||
- For identifying issues with our Dynamic Parimutuel Market Maker in an [excellent blog post](https://kevin.zielnicki.com/2022/02/17/manifold/) (and [associated market](https://manifold.markets/kjz/will-manifolds-developers-agree-wit)), leading us to change to a different mechanism.
|
||||
|
||||
**[Pepe](https://manifold.markets/Pepe): M$10,000**
|
||||
|
||||
- For developing the function used in our Constant Function Market Maker, making it easier for us to provision liquidity compared to a CPMM.
|
||||
|
||||
**[Gurkenglas](https://manifold.markets/Gurkenglas): M$5,000**
|
||||
|
||||
- For concrete suggestions around improving our market maker algorithms, and creating useful graphs to make our different market makers more legible.
|
||||
|
||||
**[Scott Alexander](https://manifold.markets/ScottAlexander): M$5,000**
|
||||
|
||||
- For [developing and publicizing the idea of providing interest-free loans on each market](https://astralcodexten.substack.com/p/play-money-and-reputation-systems), helping make long-term markets more accurate.
|
||||
|
||||
**[David Glidden](https://manifold.markets/dglid): M$5,000**
|
||||
|
||||
- For taking on the mantle of [@MetaculusBot](https://manifold.markets/MetaculusBot), which allows traders access to a wider spread of topics, and permits head-to-head comparisons between our prediction markets and other forecasting platforms.
|
||||
|
||||
**[Isaac King](https://manifold.markets/IsaacKing): M$5,000**
|
||||
|
||||
- For [compiling an FAQ](https://outsidetheasylum.blog/manifold-markets-faq/) that answers a variety of questions that new users commonly face, and also inspiring us to move to [this open-source docs platform](https://docs.manifold.markets/).
|
||||
|
||||
**[Blazer](https://manifold.markets/BlazingDarkness/was-it-an-unpleasant-surprise-when): M$2,500**
|
||||
|
||||
- For [calling out our mistake](https://manifold.markets/BlazingDarkness/was-it-an-unpleasant-surprise-when) in retroactively publicizing the market creator’s trades, leading us to revert this feature entirely.
|
||||
|
||||
⛑️ _Awarded 2022-01-09_
|
||||
|
||||
**[Duncan](https://manifold.markets/Duncan): USD $50**
|
||||
|
||||
- For identifying and confidentially reporting an exploit where entering negative numbers into the trade box would allow the trade to go through.
|
||||
- _Note: this was denominated in USD, as it predated the creation of our bounty program._
|
||||
|
||||
## Final note
|
||||
|
||||
If a particular contribution isn't listed here, that doesn't mean we didn't really appreciate it. There’s so much great work by our community; we aren't always able to catch them all!
|
||||
|
||||
If you feel that someone's exceptional contribution has fallen through the cracks (including your own!), please consider creating a market for “Will <X\> be recognized for a Manifold bounty?” and posting it on our Discord. Thanks!
|
||||
|
||||
## See also
|
||||
|
||||
- [Will Manifold implement retroactive public goods funding by June 1?](https://manifold.markets/Austin/will-manifold-implement-retroactive)
|
||||
- [Bounties as described on LessWrong](https://www.lesswrong.com/tag/bounties-active)
|
||||
- Mistakes pages we admire: [Scott Alexander](https://astralcodexten.substack.com/p/mistakes), [Nintil](https://nintil.com/mistakes), [80K Hours](https://80000hours.org/about/credibility/evaluations/mistakes/)
|
||||
- [Donald Knuth’s reward checks](https://en.wikipedia.org/wiki/Knuth_reward_check)
|
||||
![https://imgs.xkcd.com/comics/applied_math.png](https://imgs.xkcd.com/comics/applied_math.png)
|
126
docs/docs/faq.md
126
docs/docs/faq.md
|
@ -1,126 +0,0 @@
|
|||
# Community FAQ
|
||||
|
||||
## General
|
||||
|
||||
### Do I have to pay real money in order to participate?
|
||||
|
||||
Nope! Each account starts with a free 1000 mana (or M$1000 for short). If you invest it wisely, you can increase your total without ever needing to put any real money into the site.
|
||||
|
||||
### Can M$ be sold for real money?
|
||||
|
||||
No. Gambling laws put many restrictions on real-money prediction markets, so Manifold uses play money instead.
|
||||
|
||||
You can instead redeem your Mana and we will [donate to a charity](http://manifold.markets/charity) on your behalf. Redeeming and purchasing Mana occurs at a rate of M$100 to $1.
|
||||
|
||||
### How do the free response markets work?
|
||||
|
||||
Any user can enter a response and bet on it, or they can bet on other people's responses. The response probabilities are weighted proportionally to how many people have bet on them. The market creator's ante goes into a "none of the above" pseudo-option that can't be bet on and can't be chosen as a correct answer when the market is resolved. (This means that free response markets tend to lose their creator almost their entire ante. It also means that if there are only a finite number of options that could win, traders can make guaranteed money by investing in them all equally.) See [here](https://manifoldmarkets.substack.com/p/above-the-fold-milestones-and-new) for more information.
|
||||
|
||||
### How accurate are the market probabilities?
|
||||
|
||||
In general, prediction markets are very accurate. They do have some known issues, most of which can be found on the [Wikipedia page.](https://en.wikipedia.org/wiki/Prediction_market#Accuracy). There are also a few factors that are specific to Manifold Markets:
|
||||
|
||||
- Manifold uses play money for their markets, so there's less of an incentive for people to invest safely. People often goof around with silly markets and investments that they don't expect to win M$ from.
|
||||
- Anyone can create a market on Manifold, and there's nothing preventing the creator of a market from trying to manipulate it to make a profit.
|
||||
- Manifold Markets is a new project and has a large number of individual markets, which means that many of their markets don't have many participants, sometimes less than 5 people.
|
||||
- Manifold's betting system isn't perfect and has some sources of error, discussed in detail [here](https://kevin.zielnicki.com/2022/02/17/manifold/).
|
||||
|
||||
As a general heuristic, check the total pool for the market in question. The more M$ there is in the market, the more likely it is to be accurate.
|
||||
|
||||
### Can I participate without having a Google account?
|
||||
|
||||
No. See [here](https://manifold.markets/hamnox/will-manifold-markets-add-nongoogle) for the probability that this changes.
|
||||
|
||||
## Placing and winning bets
|
||||
|
||||
### The payout probabilities I'm shown sometimes aren't right. For example if a market is at 15% and I bet M$1 on "no", it tells me that I'll make a 42% profit if I win, but the listed payout is just M$1. What's going on?
|
||||
|
||||
Payout amounts are visually rounded to the nearest M$1, and only integer amounts can be put into markets. Behind the scenes however, your balance does track fractional amounts, so you're making a M$0.42 profit on that bet. Once you win another M$0.08, that fractional M$0.5 will display as an extra M$1 in your account. (There's no way to view your exact balance, you can only see the rounded value.)
|
||||
|
||||
### What are the rules about insider trading? (Using private information about a market to make a profit.)
|
||||
|
||||
It's not only allowed, but encouraged. The whole point of a prediction market is to uncover and amplify this sort of hidden information. For example, if there's a market like "will [company] make [decision]?" and you work for that company and know what decision they're going to make, you can use that information to win M$ and make the market more accurate at the same time. (Subject to your company's policies on disclosing internal information of course.) However, if the reason you have private information is because you're colluding with the market creator, this will likely earn both of you a bad reputation and people will be less interested in participating in your markets in the future.
|
||||
|
||||
### Can I see who is buying/selling in a market?
|
||||
|
||||
All trades before June 1, 2022 are anonymous by default. Trades after that date can be viewed in the Bets tab of any market, and also on that user's profile.
|
||||
|
||||
## Creating and resolving markets
|
||||
|
||||
### Is there any benefit to creating markets?
|
||||
|
||||
You get your question answered! Plus, you earn a commission on trades in your markets.
|
||||
|
||||
### What can I create a market about?
|
||||
|
||||
Anything you want to! People ask about politics, science, gaming, and even [their personal lives](https://www.smbc-comics.com/?id=2418). Take a look at the [current list of markets](https://manifold.markets/markets) to see what sorts of things people ask about.
|
||||
|
||||
### What's the difference between a market being "closed" and being "resolved"?
|
||||
|
||||
A market being "closed" means that people can no longer place or sell bets, "locking in" the current probability. Markets close when the close date of the market is met. A market being "resolved" means that the market creator has indicated a given resolution to the market's question, such as "yes", "no", "N/A", or a certain probability. This is the point at which people are cashed out of the market. Resolving a market automatically closes it, but a market can close days, weeks, or even years before it gets resolved.
|
||||
|
||||
### What does "PROB" mean?
|
||||
|
||||
Resolving a market as "PROB" means that it's resolved at a certain probability, chosen by the market creator. PROB 100% is the same as "yes", and PROB 0% is the same as "no". For example, if a market is resolved at PROB 75%, anyone who bought "yes" at less than 75% will (usually) make a profit, and anyone who bought "yes" at greater than 75% will (usually) take a loss. Vice versa for "no". This is also shown as "MKT" in the interface and API.
|
||||
|
||||
### What happens if a market creator resolves a market incorrectly, or doesn't resolve it at all?
|
||||
|
||||
Nothing. The idea is for Manifold Markets to function with similar freedom and versatility to a Twitter poll, but with more accurate results due to the dynamics of prediction markets. Individual market resolution is not enforced by the site, so if you don't trust a certain user to judge their markets fairly, you probably shouldn't participate in their markets.
|
||||
|
||||
That being said, manifold staff may manually send reminder emails to the creators of large markets if they have not been resolved in some time. There are also some projects in the works to enable automated market resolution after some time has passed.
|
||||
|
||||
### How do I tell if a certain market creator is trustworthy?
|
||||
|
||||
Look at their market resolution history on their profile page. If their past markets have all been resolved correctly, their future ones probably will be too. You can also look at the comments on those markets to see if any traders noticed anything suspicious. You can also ask about that person in the [Manifold Markets Discord](https://discord.gg/eHQBNBqXuh). And if their profile links to their website or social media pages, you can take that into account too.
|
||||
|
||||
### Are there any content filters? What happens if someone creates an inappropriate, offensive, or [dangerous](https://en.wikipedia.org/wiki/Assassination_market) market?
|
||||
|
||||
Right now, there are no restrictions on what markets can be created. If this becomes a problem, they may change their policies.
|
||||
|
||||
### Can a market creator change the close date of their market?
|
||||
|
||||
Yes. As long as the market hasn't been resolved yet, the creator can freely change its close date. They can even reopen a market that has already closed.
|
||||
|
||||
### Is there a way to see my closed markets that I need to resolve?
|
||||
|
||||
You'll get an automated email when they close. You can also go to your profile page and select "closed" in the dropdown menu. (This will display only markets that you haven't resolved yet.)
|
||||
|
||||
### When do market creators get their commission fees?
|
||||
|
||||
When the creator resolves their market, they get the commission from all the trades that were executed in the market.
|
||||
|
||||
### How do I see markets that are currently open?
|
||||
|
||||
You can see the top markets in various categories [here](https://manifold.markets/markets).
|
||||
|
||||
### Can I bet in a market I created?
|
||||
|
||||
Yes. However if you're doing things that the community would perceive as "shady", such as putting all your money on the correct resolution immediately before closing the market, people may be more reluctant to participate in your markets in the future. Betting "normally" in your own market is fine though.
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
### How do I report bugs or ask for new features?
|
||||
|
||||
Contact them via [email](mailto:info@manifold.markets), post in their [Discord](https://discord.gg/eHQBNBqXuh), or create a market about that bug/feature in order to draw more attention to it and get community input.
|
||||
|
||||
If you don't mind putting in a little work, fork the code and open a [pull request](https://github.com/manifoldmarkets/manifold/pulls) on GitHub.
|
||||
|
||||
### How can I get notified of new developments?
|
||||
|
||||
Being a very recent project, Manifold is adding new features and tweaking existing ones quite frequently. You can keep up with changes by subscribing to their [Substack](https://manifoldmarkets.substack.com/), or joining their [Discord server](https://discord.gg/eHQBNBqXuh).
|
||||
|
||||
### Is there an app?
|
||||
|
||||
No, but the website is designed responsively and looks great on mobile.
|
||||
|
||||
### Does Manifold have an API for programmers?
|
||||
|
||||
Yep. Documentation is [here](https://docs.manifold.markets/api).
|
||||
|
||||
### If I have a question that isn't answered here, where can I ask it?
|
||||
|
||||
You can contact Manifold Markets via [email](mailto:info@manifold.markets) or post in their [Discord](https://discord.gg/eHQBNBqXuh). Once you have an answer, please consider updating this FAQ via "Edit this page" on Github!
|
||||
|
||||
## Credits
|
||||
|
||||
This FAQ was originally compiled by [Isaac King](https://outsidetheasylum.blog/manifold-markets-faq/).
|
|
@ -1,109 +0,0 @@
|
|||
# Guide to Market Types
|
||||
|
||||
# Market Mechanisms
|
||||
|
||||
Historically, Manifold used a special type of automated market maker based on a dynamic pari-mutuel (DPM) betting
|
||||
system. Free response and numeric markets still use this system. Binary markets created prior to March 15, 2022 used
|
||||
this system, but all of those markets have since closed.
|
||||
|
||||
Binary markets created after March 15 use a constant-function market maker which holds constant the weighted geometric
|
||||
mean, with weights equal to the probabilities chosen by the market creator at creation. This design was inspired by
|
||||
Uniswap's CPMM and a suggestion from Manifold user Pepe. The benefit of this approach is that the payout for any bet
|
||||
is fixed at purchase time - 100 shares of YES will always return M$100 if YES is chosen.
|
||||
|
||||
Free response markets (and the depreciated numeric markets) still use the DPM system, as they have discrete "buckets"
|
||||
for the pool to be sorted into.
|
||||
|
||||
## Market Creation
|
||||
|
||||
- Users can create a market on any question they want.
|
||||
- When a user creates a market, they must choose a close date, after which trading will halt.
|
||||
- They must also pay a M$100 market creation fee, which is used as liquidity to subsidize trading on the market.
|
||||
- The market creator will earn a commission on all bets placed in the market.
|
||||
- The market creator is responsible for resolving each market in a timely manner. All fees earned as a commission will be paid out after resolution.
|
||||
- Creators can also resolve N/A to cancel all transactions and reverse all transactions made on the market - this includes profits from selling shares.
|
||||
|
||||
# Binary Markets
|
||||
|
||||
## Binary Markets: Overview
|
||||
|
||||
- Binary markets are structured around a question with a binary outcome, such as:
|
||||
- [Will Bitcoin be worth more than $60,000 on Jan 1, 2022 at 12 am ET?](https://manifold.markets/SG/will-bitcoin-be-worth-more-than-600)
|
||||
- [Will Manifold Markets have over $1M in revenue by Jan 1st, 2023?](https://manifold.markets/ManifoldMarkets/will-mantic-markets-have-over-1m)
|
||||
- [Will we discover life on Mars before 2024?](https://manifold.markets/LarsDoucet/will-we-discover-life-on-mars-befor)
|
||||
- Some binary markets are used as quasi-numeric markets, such as:
|
||||
- [How many additional subscribers will my newsletter have by the end of February?](https://manifold.markets/Nu%C3%B1oSempere/how-many-additional-subscribers-wil)
|
||||
- [How many new signups will Manifold have at the end of launch day?](https://manifold.markets/ManifoldMarkets/how-many-new-signups-will-manifold)
|
||||
- [What day will US Covid deaths peak in February?](https://manifold.markets/JamesGrugett/what-day-will-us-covid-deaths-peak)
|
||||
- These markets are made possible by the MKT option described below.
|
||||
|
||||
## Binary Markets: Betting & Payouts
|
||||
|
||||
- Traders can place a bet on either YES or NO and receive shares in the outcome in return.
|
||||
- Betting on YES will increase the market’s implied probability; betting on NO will decrease the probability.
|
||||
- Manifold's automated market automatically adjusts the market probability after each trade and determines how many shares a user will get for their bet.
|
||||
- You can sell back your shares for cash. If you sell YES shares, the market probability will go down. If you sell NO shares, the probability will go up.
|
||||
- 1 YES share = M$1 if the event happens. 1 NO share = M$1 if the event does not happen.
|
||||
- Notice that 1 YES share + 1 NO share = M$1. If you ever get multiple YES and NO shares, they will cancel out and you will be left with cash.
|
||||
- When the market is resolved, you will be paid out according to your shares. If you own 100 YES shares, if the event resolves YES, you will earn M$100. (If the event resolves NO, you will earn M$0).
|
||||
- The creator of each market is responsible for resolving each market. They can resolve to YES, NO, MKT, or N/A.
|
||||
- Resolving to MKT allows the creator to choose a percentage. The payout for any YES share is multiplied by this percentage, and vice versa for NO.
|
||||
- For example, if a market resolves to MKT at 30%, if you have 100 shares of YES you will receive `M$100 * 30% = M$30`.
|
||||
- In the same situation as above, if you have 100 shares of NO you will receive `M$100 * (100% - 30%) = M$70`.
|
||||
- Note that even in this instance, 1 YES share plus 1 NO share still equals M$1.
|
||||
|
||||
## Binary Markets: Liquidity
|
||||
|
||||
- The liquidity in a market is the amount of capital available for traders to trade against. The more liquidity, the greater incentive there is for traders to bet, and the more accurate the market should be.
|
||||
- When a market is created, the creation fee (also called the ante or subsidy) is used to fill the liquiity pool. This happens whether the creation fee is paid by the user or by Manifold for the daily free market.
|
||||
- Behind the scenes, when a bet is placed the CPMM mechanism does [a bunch of math](http://bit.ly/maniswap). The end result is that for each M$1 bet, 1 YES share and 1 NO share is created. Some amount of shares are then given to the user who made the bet, and the rest are stored in the liquidity pool.
|
||||
Due to this mechansim, the number of YES shares in the whole market always equals the number of NO shares.
|
||||
- You can manually add liquidity to any market to increase the incentives for traders to participate. You can think of added liquidity as a subsidy for getting your question answered. You can do this by opening up the market info popup window located in the (...) section of the header on the market page.
|
||||
- Adding liquidity provides you with a number of YES and NO shares, which can be withdrawn from the same interface. These shares resolve to M$ like normal when the market resolves, which will return you some amount of your investment.
|
||||
- If the market moves significantly in either direction, your liquidity will become significantly less valuable. You are currently very unlikely to make money by investing liquidity in a market, it is a way to subsidize a market and encourage more people to bet, to achieve a more accurate answer.
|
||||
- Adding liquidity to a market also makes it require more capital to move the market, so if you want to subsidize a market, first make sure the market price is roughly where you think it should be.
|
||||
|
||||
# Free-Response Markets
|
||||
|
||||
## Free-Response Markets: Overview
|
||||
|
||||
- Free-response markets are structured around a question with a multiple outcomes, such as:
|
||||
- [Which team will win the NBA Finals 2022?](https://manifold.markets/howtodowtle/which-team-will-win-the-nba-finals)
|
||||
- [Who will win "Top Streaming Songs Artist" at the 2022 Billboard Music Awards?](https://manifold.markets/Predictor/who-will-win-top-streaming-songs-ar)
|
||||
- [What life improvement intervention suggested would I found most useful?](https://manifold.markets/vlad/what-life-improvement-intervention)
|
||||
- Some free-response markets are used as quasi-numeric markets, such as:
|
||||
- [What day will Russia invade Ukraine?](https://manifold.markets/Duncan/what-day-will-russia-invade-ukraine)
|
||||
- [What will inflation be in March?](https://manifold.markets/ManifoldMarkets/what-will-inflation-be-in-march)
|
||||
- [How many Manifold team members in the Bahamas will test positive for COVID?](https://manifold.markets/Sinclair/how-many-manifold-team-members-in-t)
|
||||
|
||||
## Free-Response Markets: Betting & Payouts
|
||||
|
||||
- Markets are structured around a list of answers, any of which can be bet on.
|
||||
- When a Free Response market is created, the market creation fee goes into a hidden answer called the Ante and gets paid to the winner(s), to subsidize the market and create an incentive to bet. This happens whether the creation fee is paid by the user or by Manifold for the daily free market.
|
||||
- This hidden answer is why a market's probabilities will not add up to 100%.
|
||||
- If you want to further subsidize a market, it's customary to create an ANTE answer and put money in that.
|
||||
- Anyone can add answers to a market as long as they stake some amount of M$ on it. Traders can place a bet on any answer and receive shares in the outcome in return.
|
||||
- When a user places a bet, their M$ goes into the market's pool and they receive a certain amount of shares of the selected answer.
|
||||
- When the market is resolved, you will be paid out according to your shares. If the creator resolves to answer #1, the entire pool is divided up amongst the users who bet on answer #1 proportional to their shares.
|
||||
- The creator of each market is responsible for resolving each market. They can resolve to any single answer, or even multiple answers.
|
||||
- Resolving to multiple answers allows the creator to choose a percentage for each selected answer (or distribute equally). The payout for any answer is taken from the amount of the total pool allocated to that answer.
|
||||
- For example, let's take a free-response market with many answers. The pool for this market is $500, and you own 100 out of 500 total shares of answer #1.
|
||||
- If the creator resolves to answer #1 only, you will receive `M$500 * (100 / 500) = M$100`.
|
||||
- If the creator resolves 50% to answer #1 and 50% to answer #2, you will receive `(M$500 * 50%) * (100 / 500) = M$50`.
|
||||
- Note that your payout is dependent on the total number of shares, and thus may decrease if more people buy shares in that answer.
|
||||
|
||||
# Fees
|
||||
|
||||
- Manifold charges fees on each trade. They are automatically calculated and baked into the number of shares you receive when you place a bet.
|
||||
- Our CPMM fee schedule is currently: `10% * (1 - post-bet probability) * bet amount`
|
||||
- Note that all current binary markets use this fee schedule.
|
||||
- The post-bet probability is what the market probability would be after your bet if there were no fees.
|
||||
- Example:
|
||||
- If you bet M$100 on NO and the resulting probability without fees would be 10%, then you pay `M$100 * 10% * 10% = M$1.0`.
|
||||
- If you bet M$100 on YES and the resulting probability without fees would be 50%, then you pay `M$100 * 10% * 50% = M$5.0`.
|
||||
- 100% of this fee is used to provide a commission to the market creator, which is paid out after the market is resolved.
|
||||
- Our DPM fee schedule is currently: `5% * (1 - post-bet probability) * bet amount`
|
||||
- Note that all free-response markets use this fee schedule. The calculation for this is the same as above.
|
||||
- 4% is used to provide a commission to the market creator, which is paid out after the market is resolved. 1% is "burnt" to prevent inflation.
|
||||
- No fees are levied on sales. If you have existing shares in a binary market and buy shares on the opposite side, that is equivalent to selling your shares and you do not pay fees.
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
// @ts-check
|
||||
// Note: type annotations allow type checking and IDEs autocompletion
|
||||
|
||||
const lightCodeTheme = require('prism-react-renderer/themes/github')
|
||||
const darkCodeTheme = require('prism-react-renderer/themes/dracula')
|
||||
const math = require('remark-math')
|
||||
const katex = require('rehype-katex')
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'Manifold Docs',
|
||||
tagline: 'Learn more about the BESTEST prediction market platform~',
|
||||
url: 'https://docs.manifold.markets',
|
||||
baseUrl: '/',
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
favicon: 'https://manifold.markets/favicon.ico',
|
||||
organizationName: 'manifoldmarkets', // Usually your GitHub org/user name.
|
||||
projectName: 'docs', // Usually your repo name.
|
||||
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
routeBasePath: '/',
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
editUrl: 'https://github.com/manifoldmarkets/manifold/tree/main/docs',
|
||||
remarkPlugins: [math],
|
||||
rehypePlugins: [katex],
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve('./src/css/custom.css'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
stylesheets: [
|
||||
{
|
||||
href: 'https://cdn.jsdelivr.net/npm/katex@0.13.24/dist/katex.min.css',
|
||||
type: 'text/css',
|
||||
integrity:
|
||||
'sha384-odtC+0UGzzFL/6PNoE8rX/SPcQDXBJ+uRepguP4QkPCm2LBxH3FA3y+fKSiJ+AmM',
|
||||
crossorigin: 'anonymous',
|
||||
},
|
||||
],
|
||||
|
||||
scripts: [
|
||||
{
|
||||
src: 'https://cdn.jsdelivr.net/npm/link-summoner@1.0.2/dist/browser.min.js',
|
||||
async: 'true',
|
||||
},
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
navbar: {
|
||||
title: 'Manifold Docs',
|
||||
logo: {
|
||||
alt: 'Manifold Markets Logo',
|
||||
src: 'https://manifold.markets/logo.svg',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
docId: 'about',
|
||||
position: 'left',
|
||||
label: 'Docs',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/manifoldmarkets/manifold/tree/main/docs/docs',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
{
|
||||
title: 'Manifold',
|
||||
items: [
|
||||
{
|
||||
label: 'Manifold Markets',
|
||||
to: 'https://manifold.markets',
|
||||
},
|
||||
{
|
||||
label: 'Docs',
|
||||
to: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'Discord',
|
||||
href: 'https://discord.gg/eHQBNBqXuh',
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
href: 'https://twitter.com/manifoldmarkets',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'More',
|
||||
items: [
|
||||
{
|
||||
label: 'Blog',
|
||||
to: 'https://manifoldmarkets.substack.com',
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/manifoldmarkets/manifold/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Manifold Markets, Inc. Built with Docusaurus.`,
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
module.exports = config
|
|
@ -1,48 +0,0 @@
|
|||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"dev": "yarn start",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.0.0-beta.17",
|
||||
"@docusaurus/preset-classic": "2.0.0-beta.17",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.1.1",
|
||||
"hast-util-is-element": "1.1.0",
|
||||
"prism-react-renderer": "^1.2.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"rehype-katex": "5",
|
||||
"remark-math": "3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.0.0-beta.17",
|
||||
"@tsconfig/docusaurus": "^1.0.4",
|
||||
"@types/react": "^17.0.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||
const sidebars = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
tutorialSidebar: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['hello'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = sidebars
|
|
@ -1,70 +0,0 @@
|
|||
import React from 'react'
|
||||
import clsx from 'clsx'
|
||||
import styles from './styles.module.css'
|
||||
|
||||
type FeatureItem = {
|
||||
title: string
|
||||
Svg: React.ComponentType<React.ComponentProps<'svg'>>
|
||||
description: JSX.Element
|
||||
}
|
||||
|
||||
const FeatureList: FeatureItem[] = [
|
||||
{
|
||||
title: 'Easy to Use',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus was designed from the ground up to be easily installed and
|
||||
used to get your website up and running quickly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Focus on What Matters',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
||||
ahead and move your docs into the <code>docs</code> directory.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Powered by React',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Extend or customize your website layout by reusing React. Docusaurus can
|
||||
be extended while reusing the same header and footer.
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
function Feature({ title, Svg, description }: FeatureItem) {
|
||||
return (
|
||||
<div className={clsx('col col--4')}>
|
||||
<div className="text--center">
|
||||
<Svg className={styles.featureSvg} role="img" />
|
||||
</div>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function HomepageFeatures(): JSX.Element {
|
||||
return (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{FeatureList.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureSvg {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
article {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: #2e8555;
|
||||
--ifm-color-primary-dark: #29784c;
|
||||
--ifm-color-primary-darker: #277148;
|
||||
--ifm-color-primary-darkest: #205d3b;
|
||||
--ifm-color-primary-light: #33925d;
|
||||
--ifm-color-primary-lighter: #359962;
|
||||
--ifm-color-primary-lightest: #3cad6e;
|
||||
--ifm-code-font-size: 95%;
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #25c2a0;
|
||||
--ifm-color-primary-dark: #21af90;
|
||||
--ifm-color-primary-darker: #1fa588;
|
||||
--ifm-color-primary-darkest: #1a8870;
|
||||
--ifm-color-primary-light: #29d5b0;
|
||||
--ifm-color-primary-lighter: #32d8b4;
|
||||
--ifm-color-primary-lightest: #4fddbf;
|
||||
}
|
||||
|
||||
.docusaurus-highlight-code-line {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .docusaurus-highlight-code-line {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
title: Markdown page example
|
||||
---
|
||||
|
||||
# Markdown page example
|
||||
|
||||
You don't need React to write simple standalone pages.
|
0
docs/static/.nojekyll
vendored
0
docs/static/.nojekyll
vendored
BIN
docs/static/img/docusaurus.png
vendored
BIN
docs/static/img/docusaurus.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 5.0 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user