🧾 Limit orders! (#495)
* Simple limit order UI * Update bet schema * Restrict bet panel / bet row to only CPMMBinaryContracts (all binary DPM are resolved) * Limit orders partway implemented * Update follow leaderboard copy * Change cpmm code to take some state instead of whole contract * Write more of matching algorithm * Fill in more of placebet * Use client side contract search for emulator * More correct matching * Merge branch 'main' into limit-orders * Some cleanup * Listen for unfilled bets in bet panel. Calculate how the probability moves based on open limit orders. * Simpler switching between bet & limit bet. * Render your open bets (unfilled limit orders) * Cancel bet endpoint. * Fix build error * Rename open bets to limit bets. Tweak payout calculation * Limit probability selector to 1-99 * Deduct user balance only on each fill. Store orderAmount of bet. Timestamp of fills. * Use floating equal to check if have shares * Add limit order switcher to mobile bet dialog * Support limit orders on numeric markets * Allow CORS exception for Vercel deployments * Remove console.logs * Update user balance by new bet amount * Tweak vercel cors * Try another regexp for vercel cors * Test another vercel regex * Slight notifications refactor * Fix docs edit link (#624) * Fix docs edit link * Update github links * Small groups UX changes * Groups UX on mobile * Leaderboards => Rankings on groups * Unused vars * create: remove automatic setting of log scale * Use react-query to cache notifications (#625) * Use react-query to cache notifications * Fix imports * Cleanup * Limit unseen notifs query * Catch the bounced query * Don't use interval * Unused var * Avoid flash of page nav * Give notification question priority & 2 lines * Right justify timestamps * Rewording * Margin * Simplify error msg * Be explicit about limit for unseen notifs * Pass limit > 0 * Remove category filters * Remove category selector references * Track notification clicks * Analyze tab usage * Bold more on new group chats * Add API route for listing a bets by user (#567) * Add API route for getting a user's bets * Refactor bets API to use /bets * Update /markets to use zod validation * Update docs * Clone missing indexes from firestore * Minor notif spacing adjustments * Enable tipping on group chats w/ notif (#629) * Tweak cors regex for vercel * Your limit bets * Implement selling shares * Merge branch 'main' into limit-orders * Fix lint * Move binary search to util file * Add note that there might be closed form * Add tooltip to explain limit probability * Tweak * Cancel your limit orders if you run out of money * Don't show amount error in probability input * Require limit prob to be >= .1% and <= 99.9% * Fix focus input bug * Simplify mobile betting dialog * Move mobile limit bets list into bet dialog. * Small fixes to existing sell shares client * Lint * Refactor useSaveShares to actually read from localStorage, use less bug-prone interface. * Fix NaN error * Remove TODO * Simple bet fill notification * Tweak wording * Sort limit bets by limit prob * Padding on limit bets * Match header size Co-authored-by: Ian Philips <iansphilips@gmail.com> Co-authored-by: ahalekelly <ahalekelly@gmail.com> Co-authored-by: mantikoros <sgrugett@gmail.com> Co-authored-by: Ben Congdon <ben@congdon.dev> Co-authored-by: Austin Chen <akrolsmir@gmail.com>
This commit is contained in:
parent
fc06b03af8
commit
80ae551ca9
|
@ -4,6 +4,7 @@ export type Bet = {
|
|||
id: string
|
||||
userId: string
|
||||
contractId: string
|
||||
createdTime: number
|
||||
|
||||
amount: number // bet size; negative if SELL bet
|
||||
loanAmount?: number
|
||||
|
@ -25,9 +26,7 @@ export type Bet = {
|
|||
isAnte?: boolean
|
||||
isLiquidityProvision?: boolean
|
||||
isRedemption?: boolean
|
||||
|
||||
createdTime: number
|
||||
}
|
||||
} & Partial<LimitProps>
|
||||
|
||||
export type NumericBet = Bet & {
|
||||
value: number
|
||||
|
@ -35,4 +34,29 @@ export type NumericBet = Bet & {
|
|||
allBetAmounts: { [outcome: string]: 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,10 +1,17 @@
|
|||
import { sum, groupBy, mapValues, sumBy, zip } from 'lodash'
|
||||
import { sum, groupBy, mapValues, sumBy } from 'lodash'
|
||||
import { LimitBet } from './bet'
|
||||
|
||||
import { CPMMContract } from './contract'
|
||||
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
|
||||
|
@ -14,11 +21,11 @@ export function getCpmmProbability(
|
|||
}
|
||||
|
||||
export function getCpmmProbabilityAfterBetBeforeFees(
|
||||
contract: CPMMContract,
|
||||
state: CpmmState,
|
||||
outcome: string,
|
||||
bet: number
|
||||
) {
|
||||
const { pool, p } = contract
|
||||
const { pool, p } = state
|
||||
const shares = calculateCpmmShares(pool, p, bet, outcome)
|
||||
const { YES: y, NO: n } = pool
|
||||
|
||||
|
@ -31,12 +38,12 @@ export function getCpmmProbabilityAfterBetBeforeFees(
|
|||
}
|
||||
|
||||
export function getCpmmOutcomeProbabilityAfterBet(
|
||||
contract: CPMMContract,
|
||||
state: CpmmState,
|
||||
outcome: string,
|
||||
bet: number
|
||||
) {
|
||||
const { newPool } = calculateCpmmPurchase(contract, bet, outcome)
|
||||
const p = getCpmmProbability(newPool, contract.p)
|
||||
const { newPool } = calculateCpmmPurchase(state, bet, outcome)
|
||||
const p = getCpmmProbability(newPool, state.p)
|
||||
return outcome === 'NO' ? 1 - p : p
|
||||
}
|
||||
|
||||
|
@ -58,12 +65,8 @@ function calculateCpmmShares(
|
|||
: n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p))
|
||||
}
|
||||
|
||||
export function getCpmmFees(
|
||||
contract: CPMMContract,
|
||||
bet: number,
|
||||
outcome: string
|
||||
) {
|
||||
const prob = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet)
|
||||
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
|
||||
|
@ -78,23 +81,23 @@ export function getCpmmFees(
|
|||
}
|
||||
|
||||
export function calculateCpmmSharesAfterFee(
|
||||
contract: CPMMContract,
|
||||
state: CpmmState,
|
||||
bet: number,
|
||||
outcome: string
|
||||
) {
|
||||
const { pool, p } = contract
|
||||
const { remainingBet } = getCpmmFees(contract, bet, outcome)
|
||||
const { pool, p } = state
|
||||
const { remainingBet } = getCpmmFees(state, bet, outcome)
|
||||
|
||||
return calculateCpmmShares(pool, p, remainingBet, outcome)
|
||||
}
|
||||
|
||||
export function calculateCpmmPurchase(
|
||||
contract: CPMMContract,
|
||||
state: CpmmState,
|
||||
bet: number,
|
||||
outcome: string
|
||||
) {
|
||||
const { pool, p } = contract
|
||||
const { remainingBet, fees } = getCpmmFees(contract, bet, outcome)
|
||||
const { pool, p } = state
|
||||
const { remainingBet, fees } = getCpmmFees(state, bet, outcome)
|
||||
|
||||
const shares = calculateCpmmShares(pool, p, remainingBet, outcome)
|
||||
const { YES: y, NO: n } = pool
|
||||
|
@ -113,117 +116,111 @@ export function calculateCpmmPurchase(
|
|||
return { shares, newPool, newP, fees }
|
||||
}
|
||||
|
||||
function computeK(y: number, n: number, p: number) {
|
||||
return y ** p * n ** (1 - p)
|
||||
}
|
||||
|
||||
function sellSharesK(
|
||||
y: number,
|
||||
n: number,
|
||||
p: number,
|
||||
s: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
b: number
|
||||
) {
|
||||
return outcome === 'YES'
|
||||
? computeK(y - b + s, n - b, p)
|
||||
: computeK(y - b, n - b + s, p)
|
||||
}
|
||||
|
||||
function calculateCpmmShareValue(
|
||||
contract: CPMMContract,
|
||||
shares: number,
|
||||
// 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'
|
||||
) {
|
||||
const { pool, p } = contract
|
||||
if (outcome === 'NO') prob = 1 - prob
|
||||
|
||||
// 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
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
// 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
|
||||
})
|
||||
|
||||
// Break once we've reached max precision.
|
||||
if (mid === lowAmount || mid === highAmount) break
|
||||
return amount
|
||||
}
|
||||
|
||||
kGuess = sellSharesK(pool.YES, pool.NO, p, shares, outcome, mid)
|
||||
if (kGuess < k) {
|
||||
highAmount = mid
|
||||
} else {
|
||||
lowAmount = mid
|
||||
}
|
||||
}
|
||||
return mid
|
||||
function calculateAmountToBuyShares(
|
||||
state: CpmmState,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
// 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
|
||||
)
|
||||
|
||||
const totalShares = sumBy(takers, (taker) => taker.shares)
|
||||
return totalShares - shares
|
||||
})
|
||||
}
|
||||
|
||||
export function calculateCpmmSale(
|
||||
contract: CPMMContract,
|
||||
state: CpmmState,
|
||||
shares: number,
|
||||
outcome: string
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
if (Math.round(shares) < 0) {
|
||||
throw new Error('Cannot sell non-positive shares')
|
||||
}
|
||||
|
||||
const rawSaleValue = calculateCpmmShareValue(
|
||||
contract,
|
||||
const oppositeOutcome = outcome === 'YES' ? 'NO' : 'YES'
|
||||
const buyAmount = calculateAmountToBuyShares(
|
||||
state,
|
||||
shares,
|
||||
outcome as 'YES' | 'NO'
|
||||
oppositeOutcome,
|
||||
unfilledBets
|
||||
)
|
||||
|
||||
const { fees, remainingBet: saleValue } = getCpmmFees(
|
||||
contract,
|
||||
rawSaleValue,
|
||||
outcome === 'YES' ? 'NO' : 'YES'
|
||||
const { cpmmState, makers, takers, totalFees } = computeFills(
|
||||
oppositeOutcome,
|
||||
buyAmount,
|
||||
state,
|
||||
undefined,
|
||||
unfilledBets
|
||||
)
|
||||
|
||||
const { pool } = contract
|
||||
const { YES: y, NO: n } = pool
|
||||
// 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 { liquidityFee: fee } = fees
|
||||
const saleValue = -sumBy(saleTakers, (taker) => taker.amount)
|
||||
|
||||
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')
|
||||
return {
|
||||
saleValue,
|
||||
cpmmState,
|
||||
fees: totalFees,
|
||||
makers,
|
||||
takers: saleTakers,
|
||||
}
|
||||
|
||||
const postBetPool = { YES: newY, NO: newN }
|
||||
|
||||
const { newPool, newP } = addCpmmLiquidity(postBetPool, contract.p, fee)
|
||||
|
||||
return { saleValue, newPool, newP, fees }
|
||||
}
|
||||
|
||||
export function getCpmmProbabilityAfterSale(
|
||||
contract: CPMMContract,
|
||||
state: CpmmState,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO'
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
const { newPool } = calculateCpmmSale(contract, shares, outcome)
|
||||
return getCpmmProbability(newPool, contract.p)
|
||||
const { cpmmState } = calculateCpmmSale(state, shares, outcome, unfilledBets)
|
||||
return getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
}
|
||||
|
||||
export function getCpmmLiquidity(
|
||||
|
@ -267,11 +264,11 @@ const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => {
|
|||
}
|
||||
|
||||
export function getCpmmLiquidityPoolWeights(
|
||||
contract: CPMMContract,
|
||||
state: CpmmState,
|
||||
liquidities: LiquidityProvision[],
|
||||
excludeAntes: boolean
|
||||
) {
|
||||
const calcLiqudity = calculateLiquidityDelta(contract.p)
|
||||
const calcLiqudity = calculateLiquidityDelta(state.p)
|
||||
const liquidityShares = liquidities.map(calcLiqudity)
|
||||
const shareSum = sum(liquidityShares)
|
||||
|
||||
|
@ -293,16 +290,12 @@ export function getCpmmLiquidityPoolWeights(
|
|||
|
||||
export function getUserLiquidityShares(
|
||||
userId: string,
|
||||
contract: CPMMContract,
|
||||
state: CpmmState,
|
||||
liquidities: LiquidityProvision[],
|
||||
excludeAntes: boolean
|
||||
) {
|
||||
const weights = getCpmmLiquidityPoolWeights(
|
||||
contract,
|
||||
liquidities,
|
||||
excludeAntes
|
||||
)
|
||||
const weights = getCpmmLiquidityPoolWeights(state, liquidities, excludeAntes)
|
||||
const userWeight = weights[userId] ?? 0
|
||||
|
||||
return mapValues(contract.pool, (shares) => userWeight * shares)
|
||||
return mapValues(state.pool, (shares) => userWeight * shares)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { maxBy } from 'lodash'
|
||||
import { Bet } from './bet'
|
||||
import { Bet, LimitBet } from './bet'
|
||||
import {
|
||||
calculateCpmmSale,
|
||||
getCpmmProbability,
|
||||
|
@ -24,6 +24,7 @@ import {
|
|||
FreeResponseContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
import { floatingEqual } from './util/math'
|
||||
|
||||
export function getProbability(
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
|
@ -73,11 +74,20 @@ export function calculateShares(
|
|||
: calculateDpmShares(contract.totalShares, bet, betChoice)
|
||||
}
|
||||
|
||||
export function calculateSaleAmount(contract: Contract, bet: Bet) {
|
||||
export function calculateSaleAmount(
|
||||
contract: Contract,
|
||||
bet: Bet,
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
||||
? calculateCpmmSale(contract, Math.abs(bet.shares), bet.outcome).saleValue
|
||||
? calculateCpmmSale(
|
||||
contract,
|
||||
Math.abs(bet.shares),
|
||||
bet.outcome as 'YES' | 'NO',
|
||||
unfilledBets
|
||||
).saleValue
|
||||
: calculateDpmSaleAmount(contract, bet)
|
||||
}
|
||||
|
||||
|
@ -90,10 +100,16 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
|
|||
export function getProbabilityAfterSale(
|
||||
contract: Contract,
|
||||
outcome: string,
|
||||
shares: number
|
||||
shares: number,
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
? getCpmmProbabilityAfterSale(contract, shares, outcome as 'YES' | 'NO')
|
||||
? getCpmmProbabilityAfterSale(
|
||||
contract,
|
||||
shares,
|
||||
outcome as 'YES' | 'NO',
|
||||
unfilledBets
|
||||
)
|
||||
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
|
||||
}
|
||||
|
||||
|
@ -157,7 +173,9 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
const profit = payout + saleValue + redeemed - totalInvested
|
||||
const profitPercent = (profit / totalInvested) * 100
|
||||
|
||||
const hasShares = Object.values(totalShares).some((shares) => shares > 0)
|
||||
const hasShares = Object.values(totalShares).some(
|
||||
(shares) => !floatingEqual(shares, 0)
|
||||
)
|
||||
|
||||
return {
|
||||
invested: Math.max(0, currentInvested),
|
||||
|
|
|
@ -34,5 +34,9 @@ export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE'
|
|||
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+$/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { sumBy } from 'lodash'
|
||||
import { sortBy, sumBy } from 'lodash'
|
||||
|
||||
import { Bet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
|
||||
import { Bet, fill, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
|
||||
import {
|
||||
calculateDpmShares,
|
||||
getDpmProbability,
|
||||
|
@ -8,7 +8,12 @@ import {
|
|||
getNumericBets,
|
||||
calculateNumericDpmShares,
|
||||
} from './calculate-dpm'
|
||||
import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm'
|
||||
import {
|
||||
calculateCpmmAmountToProb,
|
||||
calculateCpmmPurchase,
|
||||
CpmmState,
|
||||
getCpmmProbability,
|
||||
} from './calculate-cpmm'
|
||||
import {
|
||||
CPMMBinaryContract,
|
||||
DPMBinaryContract,
|
||||
|
@ -17,8 +22,13 @@ import {
|
|||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
import { noFees } from './fees'
|
||||
import { addObjects } from './util/object'
|
||||
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'>
|
||||
export type BetInfo = {
|
||||
|
@ -30,38 +40,203 @@ export type BetInfo = {
|
|||
newP?: number
|
||||
}
|
||||
|
||||
export const getNewBinaryCpmmBetInfo = (
|
||||
outcome: 'YES' | 'NO',
|
||||
const computeFill = (
|
||||
amount: number,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
loanAmount: number
|
||||
outcome: 'YES' | 'NO',
|
||||
limitProb: number | undefined,
|
||||
cpmmState: CpmmState,
|
||||
matchedBet: LimitBet | undefined
|
||||
) => {
|
||||
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
|
||||
contract,
|
||||
amount,
|
||||
outcome
|
||||
)
|
||||
const prob = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
|
||||
const { pool, p, totalLiquidity } = contract
|
||||
const probBefore = getCpmmProbability(pool, p)
|
||||
const probAfter = getCpmmProbability(newPool, newP)
|
||||
|
||||
const newBet: CandidateBet = {
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
shares,
|
||||
outcome,
|
||||
fees,
|
||||
loanAmount,
|
||||
probBefore,
|
||||
probAfter,
|
||||
createdTime: Date.now(),
|
||||
if (
|
||||
limitProb !== undefined &&
|
||||
(outcome === 'YES'
|
||||
? floatingGreaterEqual(prob, limitProb) &&
|
||||
(matchedBet?.limitProb ?? 1) > limitProb
|
||||
: floatingLesserEqual(prob, limitProb) &&
|
||||
(matchedBet?.limitProb ?? 0) < limitProb)
|
||||
) {
|
||||
// No fill.
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { liquidityFee } = fees
|
||||
const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee
|
||||
const timestamp = Date.now()
|
||||
|
||||
return { newBet, newPool, newP, newTotalLiquidity }
|
||||
if (
|
||||
!matchedBet ||
|
||||
(outcome === 'YES'
|
||||
? prob < matchedBet.limitProb
|
||||
: 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 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 }
|
||||
}
|
||||
|
||||
export const computeFills = (
|
||||
outcome: 'YES' | 'NO',
|
||||
betAmount: number,
|
||||
state: CpmmState,
|
||||
limitProb: number | undefined,
|
||||
unfilledBets: LimitBet[]
|
||||
) => {
|
||||
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
|
||||
}[] = []
|
||||
|
||||
let amount = betAmount
|
||||
let cpmmState = { pool: state.pool, p: state.p }
|
||||
let totalFees = noFees
|
||||
|
||||
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.
|
||||
takers.push(taker)
|
||||
makers.push(maker)
|
||||
i++
|
||||
}
|
||||
|
||||
amount -= taker.amount
|
||||
|
||||
if (floatingEqual(amount, 0)) break
|
||||
}
|
||||
|
||||
return { takers, makers, totalFees, cpmmState }
|
||||
}
|
||||
|
||||
export const getBinaryCpmmBetInfo = (
|
||||
outcome: 'YES' | 'NO',
|
||||
betAmount: number,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
limitProb: number | undefined,
|
||||
unfilledBets: LimitBet[]
|
||||
) => {
|
||||
const { pool, p } = contract
|
||||
const { takers, makers, cpmmState, totalFees } = computeFills(
|
||||
outcome,
|
||||
betAmount,
|
||||
{ pool, p },
|
||||
limitProb,
|
||||
unfilledBets
|
||||
)
|
||||
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,
|
||||
contractId: contract.id,
|
||||
outcome,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
export const getNewBinaryDpmBetInfo = (
|
||||
|
|
|
@ -62,3 +62,4 @@ export type notification_reason_types =
|
|||
| 'unique_bettors_on_your_contract'
|
||||
| 'on_group_you_are_member_of'
|
||||
| 'tip_received'
|
||||
| 'bet_fill'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Bet } from './bet'
|
||||
import { Bet, LimitBet } from './bet'
|
||||
import {
|
||||
calculateDpmShareValue,
|
||||
deductDpmFees,
|
||||
|
@ -7,6 +7,7 @@ import {
|
|||
import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm'
|
||||
import { CPMMContract, DPMContract } from './contract'
|
||||
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
|
||||
import { sumBy } from 'lodash'
|
||||
|
||||
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
|
||||
|
||||
|
@ -78,19 +79,24 @@ export const getCpmmSellBetInfo = (
|
|||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
contract: CPMMContract,
|
||||
prevLoanAmount: number
|
||||
prevLoanAmount: number,
|
||||
unfilledBets: LimitBet[]
|
||||
) => {
|
||||
const { pool, p } = contract
|
||||
|
||||
const { saleValue, newPool, newP, fees } = calculateCpmmSale(
|
||||
const { saleValue, cpmmState, fees, makers, takers } = calculateCpmmSale(
|
||||
contract,
|
||||
shares,
|
||||
outcome
|
||||
outcome,
|
||||
unfilledBets
|
||||
)
|
||||
|
||||
const loanPaid = Math.min(prevLoanAmount, saleValue)
|
||||
const probBefore = getCpmmProbability(pool, p)
|
||||
const probAfter = getCpmmProbability(newPool, p)
|
||||
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
|
||||
const takerAmount = sumBy(takers, 'amount')
|
||||
const takerShares = sumBy(takers, 'shares')
|
||||
|
||||
console.log(
|
||||
'SELL M$',
|
||||
|
@ -104,20 +110,26 @@ export const getCpmmSellBetInfo = (
|
|||
|
||||
const newBet: CandidateBet<Bet> = {
|
||||
contractId: contract.id,
|
||||
amount: -saleValue,
|
||||
shares: -shares,
|
||||
amount: takerAmount,
|
||||
shares: takerShares,
|
||||
outcome,
|
||||
probBefore,
|
||||
probAfter,
|
||||
createdTime: Date.now(),
|
||||
loanAmount: -loanPaid,
|
||||
fees,
|
||||
fills: takers,
|
||||
isFilled: true,
|
||||
isCancelled: false,
|
||||
orderAmount: takerAmount,
|
||||
}
|
||||
|
||||
return {
|
||||
newBet,
|
||||
newPool,
|
||||
newP,
|
||||
newPool: cpmmState.pool,
|
||||
newP: cpmmState.p,
|
||||
fees,
|
||||
makers,
|
||||
takers,
|
||||
}
|
||||
}
|
||||
|
|
22
common/util/algos.ts
Normal file
22
common/util/algos.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
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
|
||||
}
|
|
@ -34,3 +34,17 @@ export function median(xs: number[]) {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { PrivateUser } from '../../common/user'
|
|||
import {
|
||||
CORS_ORIGIN_MANIFOLD,
|
||||
CORS_ORIGIN_LOCALHOST,
|
||||
CORS_ORIGIN_VERCEL,
|
||||
} from '../../common/envs/constants'
|
||||
|
||||
type Output = Record<string, unknown>
|
||||
|
@ -118,7 +119,7 @@ const DEFAULT_OPTS = {
|
|||
concurrency: 100,
|
||||
memory: '2GiB',
|
||||
cpu: 1,
|
||||
cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
||||
cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_VERCEL, CORS_ORIGIN_LOCALHOST],
|
||||
}
|
||||
|
||||
export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
|
||||
|
|
35
functions/src/cancel-bet.ts
Normal file
35
functions/src/cancel-bet.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { z } from 'zod'
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
import { LimitBet } from '../../common/bet'
|
||||
|
||||
const bodySchema = z.object({
|
||||
betId: z.string(),
|
||||
})
|
||||
|
||||
export const cancelbet = newEndpoint({}, async (req, auth) => {
|
||||
const { betId } = validate(bodySchema, req.body)
|
||||
|
||||
const result = await firestore.runTransaction(async (trans) => {
|
||||
const snap = await trans.get(
|
||||
firestore.collectionGroup('bets').where('id', '==', betId)
|
||||
)
|
||||
const betDoc = snap.docs[0]
|
||||
if (!betDoc?.exists) throw new APIError(400, 'Bet not found.')
|
||||
|
||||
const bet = betDoc.data() as LimitBet
|
||||
if (bet.userId !== auth.uid)
|
||||
throw new APIError(400, 'Not authorized to cancel bet.')
|
||||
if (bet.limitProb === undefined)
|
||||
throw new APIError(400, 'Not a limit bet: Cannot cancel.')
|
||||
if (bet.isCancelled) throw new APIError(400, 'Bet already cancelled.')
|
||||
|
||||
trans.update(betDoc.ref, { isCancelled: true })
|
||||
|
||||
return { ...bet, isCancelled: true }
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const firestore = admin.firestore()
|
|
@ -10,7 +10,7 @@ import { Contract } from '../../common/contract'
|
|||
import { getUserByUsername, getValues } from './utils'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { uniq } from 'lodash'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { Bet, LimitBet } from '../../common/bet'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { getContractBetMetrics } from '../../common/calculate'
|
||||
import { removeUndefinedProps } from '../../common/util/object'
|
||||
|
@ -382,3 +382,37 @@ export const createTipNotification = async (
|
|||
}
|
||||
return await notificationRef.set(removeUndefinedProps(notification))
|
||||
}
|
||||
|
||||
export const createBetFillNotification = async (
|
||||
fromUser: User,
|
||||
toUser: User,
|
||||
bet: Bet,
|
||||
userBet: LimitBet,
|
||||
contract: Contract,
|
||||
idempotencyKey: string
|
||||
) => {
|
||||
const fill = userBet.fills.find((fill) => fill.matchedBetId === bet.id)
|
||||
const fillAmount = fill?.amount ?? 0
|
||||
|
||||
const notificationRef = firestore
|
||||
.collection(`/users/${toUser.id}/notifications`)
|
||||
.doc(idempotencyKey)
|
||||
const notification: Notification = {
|
||||
id: idempotencyKey,
|
||||
userId: toUser.id,
|
||||
reason: 'bet_fill',
|
||||
createdTime: Date.now(),
|
||||
isSeen: false,
|
||||
sourceId: userBet.id,
|
||||
sourceType: 'bet',
|
||||
sourceUpdateType: 'updated',
|
||||
sourceUserName: fromUser.name,
|
||||
sourceUserUsername: fromUser.username,
|
||||
sourceUserAvatarUrl: fromUser.avatarUrl,
|
||||
sourceText: fillAmount.toString(),
|
||||
sourceContractCreatorUsername: contract.creatorUsername,
|
||||
sourceContractTitle: contract.question,
|
||||
sourceContractSlug: contract.slug,
|
||||
}
|
||||
return await notificationRef.set(removeUndefinedProps(notification))
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ export * from './transact'
|
|||
export * from './change-user-info'
|
||||
export * from './create-answer'
|
||||
export * from './place-bet'
|
||||
export * from './cancel-bet'
|
||||
export * from './sell-bet'
|
||||
export * from './sell-shares'
|
||||
export * from './claim-manalink'
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { keyBy } from 'lodash'
|
||||
|
||||
import { Bet } from '../../common/bet'
|
||||
import { Bet, LimitBet } from '../../common/bet'
|
||||
import { getContract, getUser, getValues } from './utils'
|
||||
import { createBetFillNotification } from './create-notification'
|
||||
import { filterDefined } from '../../common/util/array'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
|
@ -11,6 +15,8 @@ export const onCreateBet = functions.firestore
|
|||
const { contractId } = context.params as {
|
||||
contractId: string
|
||||
}
|
||||
const { eventId } = context
|
||||
|
||||
const bet = change.data() as Bet
|
||||
const lastBetTime = bet.createdTime
|
||||
|
||||
|
@ -18,4 +24,47 @@ export const onCreateBet = functions.firestore
|
|||
.collection('contracts')
|
||||
.doc(contractId)
|
||||
.update({ lastBetTime, lastUpdatedTime: Date.now() })
|
||||
|
||||
await notifyFills(bet, contractId, eventId)
|
||||
})
|
||||
|
||||
const notifyFills = async (bet: Bet, contractId: string, eventId: string) => {
|
||||
if (!bet.fills) return
|
||||
|
||||
const user = await getUser(bet.userId)
|
||||
if (!user) return
|
||||
const contract = await getContract(contractId)
|
||||
if (!contract) return
|
||||
|
||||
const matchedFills = bet.fills.filter((fill) => fill.matchedBetId !== null)
|
||||
const matchedBets = (
|
||||
await Promise.all(
|
||||
matchedFills.map((fill) =>
|
||||
getValues<LimitBet>(
|
||||
firestore.collectionGroup('bets').where('id', '==', fill.matchedBetId)
|
||||
)
|
||||
)
|
||||
)
|
||||
).flat()
|
||||
|
||||
const betUsers = await Promise.all(
|
||||
matchedBets.map((bet) => getUser(bet.userId))
|
||||
)
|
||||
const betUsersById = keyBy(filterDefined(betUsers), 'id')
|
||||
|
||||
await Promise.all(
|
||||
matchedBets.map((matchedBet) => {
|
||||
const matchedUser = betUsersById[matchedBet.userId]
|
||||
if (!matchedUser) return
|
||||
|
||||
return createBetFillNotification(
|
||||
user,
|
||||
matchedUser,
|
||||
bet,
|
||||
matchedBet,
|
||||
contract,
|
||||
eventId
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes'
|
|||
import { createNotification } from './create-notification'
|
||||
import { ReferralTxn } from '../../common/txn'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { LimitBet } from 'common/bet'
|
||||
import { QuerySnapshot } from 'firebase-admin/firestore'
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const onUpdateUser = functions.firestore
|
||||
|
@ -17,6 +19,10 @@ export const onUpdateUser = functions.firestore
|
|||
if (prevUser.referredByUserId !== user.referredByUserId) {
|
||||
await handleUserUpdatedReferral(user, eventId)
|
||||
}
|
||||
|
||||
if (user.balance <= 0) {
|
||||
await cancelLimitOrders(user.id)
|
||||
}
|
||||
})
|
||||
|
||||
async function handleUserUpdatedReferral(user: User, eventId: string) {
|
||||
|
@ -109,3 +115,15 @@ async function handleUserUpdatedReferral(user: User, eventId: string) {
|
|||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function cancelLimitOrders(userId: string) {
|
||||
const snapshot = (await firestore
|
||||
.collectionGroup('bets')
|
||||
.where('userId', '==', userId)
|
||||
.where('isFilled', '==', false)
|
||||
.get()) as QuerySnapshot<LimitBet>
|
||||
|
||||
await Promise.all(
|
||||
snapshot.docs.map((doc) => doc.ref.update({ isCancelled: true }))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
DocumentReference,
|
||||
FieldValue,
|
||||
Query,
|
||||
Transaction,
|
||||
} from 'firebase-admin/firestore'
|
||||
import { groupBy, mapValues, sumBy } from 'lodash'
|
||||
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
import {
|
||||
BetInfo,
|
||||
getNewBinaryCpmmBetInfo,
|
||||
getNewBinaryDpmBetInfo,
|
||||
getBinaryCpmmBetInfo,
|
||||
getNewMultiBetInfo,
|
||||
getNumericBetsInfo,
|
||||
} from '../../common/new-bet'
|
||||
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||
import { LimitBet } from '../../common/bet'
|
||||
import { floatingEqual } from '../../common/util/math'
|
||||
import { redeemShares } from './redeem-shares'
|
||||
import { log } from './utils'
|
||||
|
||||
|
@ -22,6 +30,7 @@ const bodySchema = z.object({
|
|||
|
||||
const binarySchema = z.object({
|
||||
outcome: z.enum(['YES', 'NO']),
|
||||
limitProb: z.number().gte(0.001).lte(0.999).optional(),
|
||||
})
|
||||
|
||||
const freeResponseSchema = z.object({
|
||||
|
@ -63,16 +72,30 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
|||
newTotalBets,
|
||||
newTotalLiquidity,
|
||||
newP,
|
||||
} = await (async (): Promise<BetInfo> => {
|
||||
if (outcomeType == 'BINARY' && mechanism == 'dpm-2') {
|
||||
const { outcome } = validate(binarySchema, req.body)
|
||||
return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount)
|
||||
} else if (
|
||||
(outcomeType == 'BINARY' || outcomeType == 'PSEUDO_NUMERIC') &&
|
||||
makers,
|
||||
} = await (async (): Promise<
|
||||
BetInfo & {
|
||||
makers?: maker[]
|
||||
}
|
||||
> => {
|
||||
if (
|
||||
(outcomeType == 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') &&
|
||||
mechanism == 'cpmm-1'
|
||||
) {
|
||||
const { outcome } = validate(binarySchema, req.body)
|
||||
return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount)
|
||||
const { outcome, limitProb } = validate(binarySchema, req.body)
|
||||
|
||||
const unfilledBetsSnap = await trans.get(
|
||||
getUnfilledBetsQuery(contractDoc)
|
||||
)
|
||||
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
|
||||
|
||||
return getBinaryCpmmBetInfo(
|
||||
outcome,
|
||||
amount,
|
||||
contract,
|
||||
limitProb,
|
||||
unfilledBets
|
||||
)
|
||||
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
|
||||
const { outcome } = validate(freeResponseSchema, req.body)
|
||||
const answerDoc = contractDoc.collection('answers').doc(outcome)
|
||||
|
@ -97,11 +120,15 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
|||
throw new APIError(400, 'Bet too large for current liquidity pool.')
|
||||
}
|
||||
|
||||
const newBalance = user.balance - amount - loanAmount
|
||||
const betDoc = contractDoc.collection('bets').doc()
|
||||
trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
|
||||
log('Created new bet document.')
|
||||
trans.update(userDoc, { balance: newBalance })
|
||||
|
||||
if (makers) {
|
||||
updateMakers(makers, betDoc.id, contractDoc, trans)
|
||||
}
|
||||
|
||||
trans.update(userDoc, { balance: FieldValue.increment(-newBet.amount) })
|
||||
log('Updated user balance.')
|
||||
trans.update(
|
||||
contractDoc,
|
||||
|
@ -112,7 +139,7 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
|||
totalBets: newTotalBets,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
collectedFees: addObjects(newBet.fees, collectedFees),
|
||||
volume: volume + amount,
|
||||
volume: volume + newBet.amount,
|
||||
})
|
||||
)
|
||||
log('Updated contract properties.')
|
||||
|
@ -127,3 +154,54 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
|||
})
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const getUnfilledBetsQuery = (contractDoc: DocumentReference) => {
|
||||
return contractDoc
|
||||
.collection('bets')
|
||||
.where('isFilled', '==', false)
|
||||
.where('isCancelled', '==', false) as Query<LimitBet>
|
||||
}
|
||||
|
||||
type maker = {
|
||||
bet: LimitBet
|
||||
amount: number
|
||||
shares: number
|
||||
timestamp: number
|
||||
}
|
||||
export const updateMakers = (
|
||||
makers: maker[],
|
||||
takerBetId: string,
|
||||
contractDoc: DocumentReference,
|
||||
trans: Transaction
|
||||
) => {
|
||||
const makersByBet = groupBy(makers, (maker) => maker.bet.id)
|
||||
for (const makers of Object.values(makersByBet)) {
|
||||
const bet = makers[0].bet
|
||||
const newFills = makers.map((maker) => {
|
||||
const { amount, shares, timestamp } = maker
|
||||
return { amount, shares, matchedBetId: takerBetId, timestamp }
|
||||
})
|
||||
const fills = [...bet.fills, ...newFills]
|
||||
const totalShares = sumBy(fills, 'shares')
|
||||
const totalAmount = sumBy(fills, 'amount')
|
||||
const isFilled = floatingEqual(totalAmount, bet.orderAmount)
|
||||
|
||||
log('Updated a matched limit bet.')
|
||||
trans.update(contractDoc.collection('bets').doc(bet.id), {
|
||||
fills,
|
||||
isFilled,
|
||||
amount: totalAmount,
|
||||
shares: totalShares,
|
||||
})
|
||||
}
|
||||
|
||||
// Deduct balance of makers.
|
||||
const spentByUser = mapValues(
|
||||
groupBy(makers, (maker) => maker.bet.userId),
|
||||
(makers) => sumBy(makers, (maker) => maker.amount)
|
||||
)
|
||||
for (const [userId, spent] of Object.entries(spentByUser)) {
|
||||
const userDoc = firestore.collection('users').doc(userId)
|
||||
trans.update(userDoc, { balance: FieldValue.increment(-spent) })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,9 @@ import { getCpmmSellBetInfo } from '../../common/sell-bet'
|
|||
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||
import { getValues } from './utils'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { floatingLesserEqual } from '../../common/util/math'
|
||||
import { getUnfilledBetsQuery, updateMakers } from './place-bet'
|
||||
import { FieldValue } from 'firebase-admin/firestore'
|
||||
|
||||
const bodySchema = z.object({
|
||||
contractId: z.string(),
|
||||
|
@ -46,14 +49,22 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
|||
const outcomeBets = userBets.filter((bet) => bet.outcome == outcome)
|
||||
const maxShares = sumBy(outcomeBets, (bet) => bet.shares)
|
||||
|
||||
if (shares > maxShares)
|
||||
if (!floatingLesserEqual(shares, maxShares))
|
||||
throw new APIError(400, `You can only sell up to ${maxShares} shares.`)
|
||||
|
||||
const { newBet, newPool, newP, fees } = getCpmmSellBetInfo(
|
||||
shares,
|
||||
const soldShares = Math.min(shares, maxShares)
|
||||
|
||||
const unfilledBetsSnap = await transaction.get(
|
||||
getUnfilledBetsQuery(contractDoc)
|
||||
)
|
||||
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
|
||||
|
||||
const { newBet, newPool, newP, fees, makers } = getCpmmSellBetInfo(
|
||||
soldShares,
|
||||
outcome,
|
||||
contract,
|
||||
prevLoanAmount
|
||||
prevLoanAmount,
|
||||
unfilledBets
|
||||
)
|
||||
|
||||
if (
|
||||
|
@ -65,11 +76,17 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
|||
}
|
||||
|
||||
const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
|
||||
const newBalance = user.balance - newBet.amount + (newBet.loanAmount ?? 0)
|
||||
const userId = user.id
|
||||
|
||||
transaction.update(userDoc, { balance: newBalance })
|
||||
transaction.create(newBetDoc, { id: newBetDoc.id, userId, ...newBet })
|
||||
updateMakers(makers, newBetDoc.id, contractDoc, transaction)
|
||||
|
||||
transaction.update(userDoc, {
|
||||
balance: FieldValue.increment(-newBet.amount),
|
||||
})
|
||||
transaction.create(newBetDoc, {
|
||||
id: newBetDoc.id,
|
||||
userId: user.id,
|
||||
...newBet,
|
||||
})
|
||||
transaction.update(
|
||||
contractDoc,
|
||||
removeUndefinedProps({
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import clsx from 'clsx'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { partition, sumBy } from 'lodash'
|
||||
import { SwitchHorizontalIcon } from '@heroicons/react/solid'
|
||||
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import {
|
||||
BinaryContract,
|
||||
CPMMBinaryContract,
|
||||
PseudoNumericContract,
|
||||
} from 'common/contract'
|
||||
import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { Col } from './layout/col'
|
||||
import { Row } from './layout/row'
|
||||
import { Spacer } from './layout/spacer'
|
||||
|
@ -18,20 +15,16 @@ import {
|
|||
formatPercent,
|
||||
formatWithCommas,
|
||||
} from 'common/util/format'
|
||||
import { getBinaryCpmmBetInfo } from 'common/new-bet'
|
||||
import { Title } from './title'
|
||||
import { User } from 'web/lib/firebase/users'
|
||||
import { Bet } from 'common/bet'
|
||||
import { Bet, LimitBet } from 'common/bet'
|
||||
import { APIError, placeBet } from 'web/lib/firebase/api-call'
|
||||
import { sellShares } from 'web/lib/firebase/api-call'
|
||||
import { AmountInput, BuyAmountInput } from './amount-input'
|
||||
import { InfoTooltip } from './info-tooltip'
|
||||
import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label'
|
||||
import {
|
||||
calculatePayoutAfterCorrectBet,
|
||||
calculateShares,
|
||||
getProbability,
|
||||
getOutcomeProbabilityAfterBet,
|
||||
} from 'common/calculate'
|
||||
import { BinaryOutcomeLabel } from './outcome-label'
|
||||
import { getProbability } from 'common/calculate'
|
||||
import { useFocus } from 'web/hooks/use-focus'
|
||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||
import {
|
||||
|
@ -39,178 +32,153 @@ import {
|
|||
getCpmmProbability,
|
||||
getCpmmFees,
|
||||
} from 'common/calculate-cpmm'
|
||||
import { getFormattedMappedValue } from 'common/pseudo-numeric'
|
||||
import {
|
||||
getFormattedMappedValue,
|
||||
getPseudoProbability,
|
||||
} from 'common/pseudo-numeric'
|
||||
import { SellRow } from './sell-row'
|
||||
import { useSaveShares } from './use-save-shares'
|
||||
import { useSaveBinaryShares } from './use-save-binary-shares'
|
||||
import { SignUpPrompt } from './sign-up-prompt'
|
||||
import { isIOS } from 'web/lib/util/device'
|
||||
import { ProbabilityInput } from './probability-input'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { removeUndefinedProps } from 'common/util/object'
|
||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||
import { LimitBets } from './limit-bets'
|
||||
import { BucketInput } from './bucket-input'
|
||||
|
||||
export function BetPanel(props: {
|
||||
contract: BinaryContract | PseudoNumericContract
|
||||
contract: CPMMBinaryContract | PseudoNumericContract
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, className } = props
|
||||
const user = useUser()
|
||||
const userBets = useUserContractBets(user?.id, contract.id)
|
||||
const { yesFloorShares, noFloorShares } = useSaveShares(contract, userBets)
|
||||
const sharesOutcome = yesFloorShares
|
||||
? 'YES'
|
||||
: noFloorShares
|
||||
? 'NO'
|
||||
: undefined
|
||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||