Deduct user balance only on each fill. Store orderAmount of bet. Timestamp of fills.

This commit is contained in:
James Grugett 2022-07-06 17:49:17 -05:00
parent a2a655063a
commit 34b80074a3
8 changed files with 60 additions and 38 deletions

View File

@ -1,4 +1,3 @@
import { Truthy } from 'lodash'
import { Fees } from './fees'
export type Bet = {
@ -27,17 +26,10 @@ export type Bet = {
isAnte?: boolean
isLiquidityProvision?: boolean
isRedemption?: boolean
// A record of each transaction that partially (or fully) fills the bet amount.
// 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?: {
// The id the bet matched against, or null if the bet was matched by
// the pool.
matchedBetId: string | null
amount: number
shares: number
}[]
fills?: fill[]
}
export type NumericBet = Bet & {
@ -48,10 +40,19 @@ export type NumericBet = Bet & {
// Binary market limit order.
export type LimitBet = Bet & {
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.
fills: Truthy<Bet['fills']>
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
}
export const MAX_LOAN_PER_CONTRACT = 20

View File

@ -1,6 +1,6 @@
import { sortBy, sumBy } from 'lodash'
import { Bet, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
import { Bet, fill, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
import {
calculateDpmShares,
getDpmProbability,
@ -62,6 +62,8 @@ const computeFill = (
return undefined
}
const timestamp = Date.now()
if (
!matchedBet ||
(outcome === 'YES'
@ -103,18 +105,19 @@ const computeFill = (
amount: poolAmount,
state: newState,
fees,
timestamp,
},
taker: {
matchedBetId: null,
shares,
amount: poolAmount,
timestamp,
},
}
}
// Fill from matchedBet.
const matchRemaining =
matchedBet.amount - sumBy(matchedBet.fills, (fill) => fill.amount)
const matchRemaining = matchedBet.orderAmount - matchedBet.amount
const shares = Math.min(
amount /
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
@ -138,6 +141,7 @@ const computeFill = (
shares *
(outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb),
shares,
timestamp,
}
const taker = {
matchedBetId: matchedBet.id,
@ -145,6 +149,7 @@ const computeFill = (
shares *
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
shares,
timestamp,
}
return { maker, taker }
}
@ -164,12 +169,13 @@ export const getBinaryCpmmBetInfo = (
console.log({ outcome, betAmount, limitProb, sortedBets })
const takers: {
matchedBetId: string | null
const takers: fill[] = []
const makers: {
bet: LimitBet
amount: number
shares: number
timestamp: number
}[] = []
const makers: { bet: LimitBet; amount: number; shares: number }[] = []
let amount = betAmount
let cpmmState = { pool: contract.pool, p: contract.p }
@ -210,13 +216,14 @@ export const getBinaryCpmmBetInfo = (
const isFilled = floatingEqual(betAmount, takerAmount)
const newBet: CandidateBet = removeUndefinedProps({
amount: betAmount,
orderAmount: betAmount,
amount: takerAmount,
shares: takerShares,
limitProb,
isFilled,
isCancelled: false,
fills: takers,
contractId: contract.id,
shares: takerShares,
outcome,
probBefore,
probAfter,

View File

@ -1,7 +1,7 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { Query } from 'firebase-admin/firestore'
import { sumBy } from 'lodash'
import { FieldValue, Query } 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'
@ -74,6 +74,7 @@ export const placebet = newEndpoint({}, async (req, auth) => {
bet: LimitBet
amount: number
shares: number
timestamp: number
}[]
}
> => {
@ -128,29 +129,44 @@ 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.')
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 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: betDoc.id, timestamp }
})
const fills = [...bet.fills, ...newFills]
const totalShares = sumBy(fills, 'shares')
const isFilled = floatingEqual(sumBy(fills, 'amount'), bet.amount)
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) })
}
}
trans.update(userDoc, { balance: newBalance })
trans.update(userDoc, { balance: FieldValue.increment(-amount) })
log('Updated user balance.')
trans.update(
contractDoc,

View File

@ -24,10 +24,7 @@ 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,
getProbability,
} from 'common/calculate'
import { getProbability } from 'common/calculate'
import { useFocus } from 'web/hooks/use-focus'
import { useUserContractBets } from 'web/hooks/use-user-bets'
import {

View File

@ -28,7 +28,6 @@ export default function BetRow(props: {
contract,
userBets
)
return (
<>

View File

@ -505,7 +505,7 @@ export function ContractBetsTable(props: {
const { contract, className, isYourBets } = props
const bets = sortBy(
props.bets.filter((b) => !b.isAnte),
props.bets.filter((b) => !b.isAnte && b.amount !== 0),
(bet) => bet.createdTime
).reverse()

View File

@ -31,7 +31,9 @@ export function ContractActivity(props: {
const comments = updatedComments ?? props.comments
const updatedBets = useBets(contract.id)
const bets = (updatedBets ?? props.bets).filter((bet) => !bet.isRedemption)
const bets = (updatedBets ?? props.bets).filter(
(bet) => !bet.isRedemption && bet.amount !== 0
)
const items = getSpecificContractActivityItems(
contract,
bets,

View File

@ -28,7 +28,7 @@ export function LimitBets(props: { bets: LimitBet[]; className?: string }) {
function LimitBet(props: { bet: LimitBet }) {
const { bet } = props
const filledAmount = sumBy(bet.fills, (fill) => fill.amount)
const { orderAmount, amount, limitProb, outcome } = bet
const [isCancelling, setIsCancelling] = useState(false)
const onCancel = () => {
@ -40,11 +40,11 @@ function LimitBet(props: { bet: LimitBet }) {
<tr>
<td>
<div className="pl-2">
<BinaryOutcomeLabel outcome={bet.outcome as 'YES' | 'NO'} />
<BinaryOutcomeLabel outcome={outcome as 'YES' | 'NO'} />
</div>
</td>
<td>{formatMoney(bet.amount - filledAmount)}</td>
<td>{formatPercent(bet.limitProb)}</td>
<td>{formatMoney(orderAmount - amount)}</td>
<td>{formatPercent(limitProb)}</td>
<td>
{isCancelling ? (
<LoadingIndicator />