diff --git a/common/new-bet.ts b/common/new-bet.ts index 1be408cd..077f3431 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -37,17 +37,19 @@ export type BetInfo = { const computeFill = ( betAmount: number, outcome: 'YES' | 'NO', - limitProb: number, + limitProb: number | undefined, cpmmState: CpmmState, matchedBet: LimitBet ) => { const prob = getCpmmProbability(cpmmState.pool, cpmmState.p) if ( - outcome === 'YES' + limitProb !== undefined && + (outcome === 'YES' ? Math.min(prob, matchedBet.limitProb) > limitProb - : Math.max(prob, matchedBet.limitProb) < limitProb + : Math.max(prob, matchedBet.limitProb) < limitProb) ) { + // No fill. return undefined } @@ -59,20 +61,19 @@ const computeFill = ( // Fill from pool. const limit = outcome === 'YES' - ? Math.min(matchedBet.limitProb, limitProb) - : Math.max(matchedBet.limitProb, limitProb) - const amount = calculateCpmmAmount(cpmmState, limit, 'YES') + ? Math.min(matchedBet.limitProb, limitProb ?? Infinity) + : Math.max(matchedBet.limitProb, limitProb ?? -Infinity) + const amount = calculateCpmmAmount(cpmmState, limit, outcome) const { shares, newPool, newP, fees } = calculateCpmmPurchase( cpmmState, amount, - 'YES' + outcome ) const newState = { pool: newPool, p: newP } return { maker: { - betId: 'bet', matchedBetId: null, shares, amount, @@ -80,7 +81,6 @@ const computeFill = ( fees, }, taker: { - betId: 'bet', matchedBetId: null, shares, amount, @@ -88,17 +88,16 @@ const computeFill = ( } } - // Fill from bet. + // Fill from matchedBet. const amount = Math.min(betAmount, matchedBet.amount) const shares = matchedBet.shares * (amount / matchedBet.amount) const maker = { - betId: matchedBet.id, - matchedBetId: 'bet', + bet: matchedBet, + matchedBetId: 'taker', amount, shares, } const taker = { - betId: 'bet', matchedBetId: matchedBet.id, amount, shares, @@ -106,20 +105,33 @@ const computeFill = ( return { maker, taker } } -export const getBinaryCpmmLimitBetInfo = ( +export const getBinaryCpmmBetInfo = ( outcome: 'YES' | 'NO', betAmount: number, contract: CPMMBinaryContract, - limitProb: number, + limitProb: number | undefined, unfilledBets: LimitBet[] // Sorted by limitProb, createdTime ) => { - const fills: LimitBet['fills'] = [] + const takers: { + matchedBetId: string | null + amount: number + shares: number + }[] = [] + const makers: { bet: LimitBet; amount: number; shares: number }[] = [] let cpmmState = { pool: contract.pool, p: contract.p } let totalFees = noFees - for (const bet of unfilledBets) { - const fill = computeFill(betAmount, outcome, limitProb, cpmmState, bet) + let i = 0 + while (i < unfilledBets.length) { + const matchedBet = unfilledBets[i] + const fill = computeFill( + betAmount, + outcome, + limitProb, + cpmmState, + matchedBet + ) if (!fill) break const { maker, taker } = fill @@ -130,12 +142,43 @@ export const getBinaryCpmmLimitBetInfo = ( cpmmState = maker.state totalFees = addObjects(totalFees, maker.fees) } else { - fills.push(maker) + makers.push(maker) + i++ } - fills.push(taker) + takers.push(taker) } - return { fills, cpmmState, totalFees } + 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 = Math.abs(betAmount - takerAmount) < 0.000000001 + + const newBet = { + amount: betAmount, + isFilled, + fills: takers, + contractId: contract.id, + shares: takerShares, + 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 getNewBinaryCpmmBetInfo = ( diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 6d58b5f3..9b6a550d 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -6,8 +6,7 @@ import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract' import { User } from '../../common/user' import { BetInfo, - getBinaryCpmmLimitBetInfo, - getNewBinaryCpmmBetInfo, + getBinaryCpmmBetInfo, getNewBinaryDpmBetInfo, getNewMultiBetInfo, getNumericBetsInfo, @@ -17,6 +16,7 @@ import { redeemShares } from './redeem-shares' import { log } from './utils' import { LimitBet } from 'common/bet' import { Query } from 'firebase-admin/firestore' +import { sumBy } from 'lodash' const bodySchema = z.object({ contractId: z.string(), @@ -70,18 +70,28 @@ export const placebet = newEndpoint(['POST'], async (req, auth) => { newTotalBets, newTotalLiquidity, newP, - } = await (async (): Promise => { + makers, + } = await (async (): Promise< + BetInfo & { + makers?: { + bet: LimitBet + amount: number + shares: number + }[] + } + > => { if (outcomeType == 'BINARY' && mechanism == 'dpm-2') { const { outcome } = validate(binarySchema, req.body) return getNewBinaryDpmBetInfo(outcome, amount, contract, loanAmount) } else if (outcomeType == 'BINARY' && mechanism == 'cpmm-1') { const { outcome, limitProb } = validate(binarySchema, req.body) + const boundedLimitProb = limitProb ?? (outcome === 'YES' ? 1 : 0) const unfilledBetsQuery = contractDoc .collection('bets') .where('outcome', '==', outcome === 'YES' ? 'NO' : 'YES') .where('isFilled', '==', false) .where('isCancelled', '==', false) - .where('limitProb', outcome === 'YES' ? '<=' : '>=', limitProb) + .where('limitProb', outcome === 'YES' ? '<=' : '>=', boundedLimitProb) .orderBy('createdTime', 'desc') .orderBy( 'limitProb', @@ -91,16 +101,13 @@ export const placebet = newEndpoint(['POST'], async (req, auth) => { const unfilledBetsSnap = await trans.get(unfilledBetsQuery) const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data()) - if (limitProb !== undefined) { - getBinaryCpmmLimitBetInfo( - outcome, - amount, - contract, - limitProb, - unfilledBets, - ) - } - return getNewBinaryCpmmBetInfo(outcome, amount, contract, loanAmount) + 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) @@ -129,6 +136,25 @@ export const placebet = newEndpoint(['POST'], async (req, auth) => { const betDoc = contractDoc.collection('bets').doc() trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet }) log('Created new bet document.') + + if (makers) { + for (const maker of makers) { + const { bet, amount, shares } = maker + const newFill = { amount, shares, matchedBetId: betDoc.id } + const fills = [...bet.fills, newFill] + const totalShares = sumBy(fills, 'shares') + const isFilled = + Math.abs(sumBy(fills, 'amount') - bet.amount) < 0.0000001 + + log('Updated a matched limit bet.') + trans.update(contractDoc.collection('bets').doc(bet.id), { + fills, + isFilled, + shares: totalShares, + }) + } + } + trans.update(userDoc, { balance: newBalance }) log('Updated user balance.') trans.update(