manifold/common/calculate-cpmm.ts
James Grugett f533d9bfcb
Verify balance of limit order "makers" (#1007)
* Fetch balance of users with open limit orders & cancel orders with insufficient balance

* Fix imports

* Fix bugs

* Fix a bug

* Remove redundant cast

* buttons overlaying content fix (#1005)

* buttons overlaying content fix

* stats: round DAU number

* made set width for portfolio/profit fields (#1006)

* tournaments: included resolved markets

* made delete red, moved button for regular posts (#1008)

* Fix localstorage saved user being overwritten on every page load

* Market page: Show no right panel while user loading

* Don't flash sign in button if user is loading

* election map coloring

* market group modal scroll fix (#1009)

* midterms: posititoning, make mobile friendly

* Un-daisy share buttons (#1010)

* Make embed and challenge buttons non-daisyui

* Allow link Buttons. Change tweet, dupe buttons.

* lint

* don't insert extra lines when upload photos

* Map fixes (#1011)

* usa map: fix sizing

* useSetIframeBackbroundColor

* preload contracts

* seo

* remove hook

* turn off sprig on dev

* Render timestamp only on client to prevent error of server not matching client

* Make sized container have default height so graph doesn't jump

* midterms: use null in static props

* Create common card component (#1012)

* Create common card component

* lint

* add key prop to pills

* redirect to /home after login

* create market: use transaction

* card: reduce border size

* Update groupContracts in db trigger

* Default sort to best

* Save comment sort per user rather than per contract

* Refactor Pinned Items into a reusable component

* Revert "create market: use transaction"

This reverts commit e1f24f24a9.

* Mark @v with a (Bot) label

* fix padding on daily movers

* fix type errors

* Wrap sprig init in check for window

* unindex date-docs from search engines

* Auto-prettification

* compute elasticity

* change dpm elasticity

* Fix google lighthouse issues (#1013)

* don't hide free response panel on open resolve

* liquidity sort

* Limit order trade log: '/' to 'of'. Remove 'of' in 'of YES'.

* Date doc: Toggle to disable creating a prediction market

* Listen for date doc changes

* Fix merge error

* Don't cancel all a users limit orders if they go negative

Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com>
Co-authored-by: mantikoros <95266179+mantikoros@users.noreply.github.com>
Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: Pico2x <pico2x@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: sipec <sipec@users.noreply.github.com>
2022-10-06 22:16:49 -05:00

316 lines
8.3 KiB
TypeScript

import { sum, groupBy, mapValues, sumBy } from 'lodash'
import { LimitBet } from './bet'
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'
import { addObjects } from './util/object'
export type CpmmState = {
pool: { [outcome: string]: number }
p: number
}
export function getCpmmProbability(
pool: { [outcome: string]: number },
p: number
) {
const { YES, NO } = pool
return (p * NO) / ((1 - p) * YES + p * NO)
}
export function getCpmmProbabilityAfterBetBeforeFees(
state: CpmmState,
outcome: string,
bet: number
) {
const { pool, p } = state
const shares = calculateCpmmShares(pool, p, bet, outcome)
const { YES: y, NO: n } = pool
const [newY, newN] =
outcome === 'YES'
? [y - shares + bet, n + bet]
: [y + bet, n - shares + bet]
return getCpmmProbability({ YES: newY, NO: newN }, p)
}
export function getCpmmOutcomeProbabilityAfterBet(
state: CpmmState,
outcome: string,
bet: number
) {
const { newPool } = calculateCpmmPurchase(state, bet, outcome)
const p = getCpmmProbability(newPool, state.p)
return outcome === 'NO' ? 1 - p : p
}
// before liquidity fee
function calculateCpmmShares(
pool: {
[outcome: string]: number
},
p: number,
bet: number,
betChoice: string
) {
const { YES: y, NO: n } = pool
const k = y ** p * n ** (1 - p)
return betChoice === 'YES'
? // https://www.wolframalpha.com/input?i=%28y%2Bb-s%29%5E%28p%29*%28n%2Bb%29%5E%281-p%29+%3D+k%2C+solve+s
y + bet - (k * (bet + n) ** (p - 1)) ** (1 / p)
: n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p))
}
export function getCpmmFees(state: CpmmState, bet: number, outcome: string) {
const prob = getCpmmProbabilityAfterBetBeforeFees(state, outcome, bet)
const betP = outcome === 'YES' ? 1 - prob : prob
const liquidityFee = LIQUIDITY_FEE * betP * bet
const platformFee = PLATFORM_FEE * betP * bet
const creatorFee = CREATOR_FEE * betP * bet
const fees: Fees = { liquidityFee, platformFee, creatorFee }
const totalFees = liquidityFee + platformFee + creatorFee
const remainingBet = bet - totalFees
return { remainingBet, totalFees, fees }
}
export function calculateCpmmSharesAfterFee(
state: CpmmState,
bet: number,
outcome: string
) {
const { pool, p } = state
const { remainingBet } = getCpmmFees(state, bet, outcome)
return calculateCpmmShares(pool, p, remainingBet, outcome)
}
export function calculateCpmmPurchase(
state: CpmmState,
bet: number,
outcome: string
) {
const { pool, p } = state
const { remainingBet, fees } = getCpmmFees(state, bet, outcome)
const shares = calculateCpmmShares(pool, p, remainingBet, outcome)
const { YES: y, NO: n } = pool
const { liquidityFee: fee } = fees
const [newY, newN] =
outcome === 'YES'
? [y - shares + remainingBet + fee, n + remainingBet + fee]
: [y + remainingBet + fee, n - shares + remainingBet + fee]
const postBetPool = { YES: newY, NO: newN }
const { newPool, newP } = addCpmmLiquidity(postBetPool, p, fee)
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 calculateAmountToBuyShares(
state: CpmmState,
shares: number,
outcome: 'YES' | 'NO',
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: 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
)
const totalShares = sumBy(takers, (taker) => taker.shares)
return totalShares - shares
})
}
export function calculateCpmmSale(
state: CpmmState,
shares: number,
outcome: 'YES' | 'NO',
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) {
if (Math.round(shares) < 0) {
throw new Error('Cannot sell non-positive shares')
}
const oppositeOutcome = outcome === 'YES' ? 'NO' : 'YES'
const buyAmount = calculateAmountToBuyShares(
state,
shares,
oppositeOutcome,
unfilledBets,
balanceByUserId
)
const { cpmmState, makers, takers, totalFees, ordersToCancel } = computeFills(
oppositeOutcome,
buyAmount,
state,
undefined,
unfilledBets,
balanceByUserId
)
// 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 saleValue = -sumBy(saleTakers, (taker) => taker.amount)
return {
saleValue,
cpmmState,
fees: totalFees,
makers,
takers: saleTakers,
ordersToCancel,
}
}
export function getCpmmProbabilityAfterSale(
state: CpmmState,
shares: number,
outcome: 'YES' | 'NO',
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) {
const { cpmmState } = calculateCpmmSale(
state,
shares,
outcome,
unfilledBets,
balanceByUserId
)
return getCpmmProbability(cpmmState.pool, cpmmState.p)
}
export function getCpmmLiquidity(
pool: { [outcome: string]: number },
p: number
) {
const { YES, NO } = pool
return YES ** p * NO ** (1 - p)
}
export function addCpmmLiquidity(
pool: { [outcome: string]: number },
p: number,
amount: number
) {
const prob = getCpmmProbability(pool, p)
//https://www.wolframalpha.com/input?i=p%28n%2Bb%29%2F%28%281-p%29%28y%2Bb%29%2Bp%28n%2Bb%29%29%3Dq%2C+solve+p
const { YES: y, NO: n } = pool
const numerator = prob * (amount + y)
const denominator = amount - n * (prob - 1) + prob * y
const newP = numerator / denominator
const newPool = { YES: y + amount, NO: n + amount }
const oldLiquidity = getCpmmLiquidity(pool, newP)
const newLiquidity = getCpmmLiquidity(newPool, newP)
const liquidity = newLiquidity - oldLiquidity
return { newPool, liquidity, newP }
}
const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => {
const oldLiquidity = getCpmmLiquidity(l.pool, p)
const newPool = addObjects(l.pool, { YES: l.amount, NO: l.amount })
const newLiquidity = getCpmmLiquidity(newPool, p)
const liquidity = newLiquidity - oldLiquidity
return liquidity
}
export function getCpmmLiquidityPoolWeights(
state: CpmmState,
liquidities: LiquidityProvision[],
excludeAntes: boolean
) {
const calcLiqudity = calculateLiquidityDelta(state.p)
const liquidityShares = liquidities.map(calcLiqudity)
const shareSum = sum(liquidityShares)
const weights = liquidityShares.map((shares, i) => ({
weight: shares / shareSum,
providerId: liquidities[i].userId,
}))
const includedWeights = excludeAntes
? weights.filter((_, i) => !liquidities[i].isAnte)
: weights
const userWeights = groupBy(includedWeights, (w) => w.providerId)
const totalUserWeights = mapValues(userWeights, (userWeight) =>
sumBy(userWeight, (w) => w.weight)
)
return totalUserWeights
}
export function getUserLiquidityShares(
userId: string,
state: CpmmState,
liquidities: LiquidityProvision[],
excludeAntes: boolean
) {
const weights = getCpmmLiquidityPoolWeights(state, liquidities, excludeAntes)
const userWeight = weights[userId] ?? 0
return mapValues(state.pool, (shares) => userWeight * shares)
}