Deduct user balance only on each fill. Store orderAmount of bet. Timestamp of fills.
This commit is contained in:
parent
a2a655063a
commit
34b80074a3
|
@ -1,4 +1,3 @@
|
||||||
import { Truthy } from 'lodash'
|
|
||||||
import { Fees } from './fees'
|
import { Fees } from './fees'
|
||||||
|
|
||||||
export type Bet = {
|
export type Bet = {
|
||||||
|
@ -27,17 +26,10 @@ export type Bet = {
|
||||||
isAnte?: boolean
|
isAnte?: boolean
|
||||||
isLiquidityProvision?: boolean
|
isLiquidityProvision?: boolean
|
||||||
isRedemption?: boolean
|
isRedemption?: boolean
|
||||||
|
|
||||||
// A record of each transaction that partially (or fully) fills the bet amount.
|
// 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.
|
// 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.
|
// Non-limit orders can also be filled by matching with multiple limit orders.
|
||||||
fills?: {
|
fills?: fill[]
|
||||||
// The id the bet matched against, or null if the bet was matched by
|
|
||||||
// the pool.
|
|
||||||
matchedBetId: string | null
|
|
||||||
amount: number
|
|
||||||
shares: number
|
|
||||||
}[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NumericBet = Bet & {
|
export type NumericBet = Bet & {
|
||||||
|
@ -48,10 +40,19 @@ export type NumericBet = Bet & {
|
||||||
|
|
||||||
// Binary market limit order.
|
// Binary market limit order.
|
||||||
export type LimitBet = Bet & {
|
export type LimitBet = Bet & {
|
||||||
|
orderAmount: number // Amount of limit order.
|
||||||
limitProb: number // [0, 1]. Bet to this probability.
|
limitProb: number // [0, 1]. Bet to this probability.
|
||||||
isFilled: boolean // Whether all of the bet amount has been filled.
|
isFilled: boolean // Whether all of the bet amount has been filled.
|
||||||
isCancelled: boolean // Whether to prevent any further fills.
|
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
|
export const MAX_LOAN_PER_CONTRACT = 20
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { sortBy, sumBy } from 'lodash'
|
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 {
|
import {
|
||||||
calculateDpmShares,
|
calculateDpmShares,
|
||||||
getDpmProbability,
|
getDpmProbability,
|
||||||
|
@ -62,6 +62,8 @@ const computeFill = (
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timestamp = Date.now()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!matchedBet ||
|
!matchedBet ||
|
||||||
(outcome === 'YES'
|
(outcome === 'YES'
|
||||||
|
@ -103,18 +105,19 @@ const computeFill = (
|
||||||
amount: poolAmount,
|
amount: poolAmount,
|
||||||
state: newState,
|
state: newState,
|
||||||
fees,
|
fees,
|
||||||
|
timestamp,
|
||||||
},
|
},
|
||||||
taker: {
|
taker: {
|
||||||
matchedBetId: null,
|
matchedBetId: null,
|
||||||
shares,
|
shares,
|
||||||
amount: poolAmount,
|
amount: poolAmount,
|
||||||
|
timestamp,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill from matchedBet.
|
// Fill from matchedBet.
|
||||||
const matchRemaining =
|
const matchRemaining = matchedBet.orderAmount - matchedBet.amount
|
||||||
matchedBet.amount - sumBy(matchedBet.fills, (fill) => fill.amount)
|
|
||||||
const shares = Math.min(
|
const shares = Math.min(
|
||||||
amount /
|
amount /
|
||||||
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
|
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
|
||||||
|
@ -138,6 +141,7 @@ const computeFill = (
|
||||||
shares *
|
shares *
|
||||||
(outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb),
|
(outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb),
|
||||||
shares,
|
shares,
|
||||||
|
timestamp,
|
||||||
}
|
}
|
||||||
const taker = {
|
const taker = {
|
||||||
matchedBetId: matchedBet.id,
|
matchedBetId: matchedBet.id,
|
||||||
|
@ -145,6 +149,7 @@ const computeFill = (
|
||||||
shares *
|
shares *
|
||||||
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
|
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
|
||||||
shares,
|
shares,
|
||||||
|
timestamp,
|
||||||
}
|
}
|
||||||
return { maker, taker }
|
return { maker, taker }
|
||||||
}
|
}
|
||||||
|
@ -164,12 +169,13 @@ export const getBinaryCpmmBetInfo = (
|
||||||
|
|
||||||
console.log({ outcome, betAmount, limitProb, sortedBets })
|
console.log({ outcome, betAmount, limitProb, sortedBets })
|
||||||
|
|
||||||
const takers: {
|
const takers: fill[] = []
|
||||||
matchedBetId: string | null
|
const makers: {
|
||||||
|
bet: LimitBet
|
||||||
amount: number
|
amount: number
|
||||||
shares: number
|
shares: number
|
||||||
|
timestamp: number
|
||||||
}[] = []
|
}[] = []
|
||||||
const makers: { bet: LimitBet; amount: number; shares: number }[] = []
|
|
||||||
|
|
||||||
let amount = betAmount
|
let amount = betAmount
|
||||||
let cpmmState = { pool: contract.pool, p: contract.p }
|
let cpmmState = { pool: contract.pool, p: contract.p }
|
||||||
|
@ -210,13 +216,14 @@ export const getBinaryCpmmBetInfo = (
|
||||||
const isFilled = floatingEqual(betAmount, takerAmount)
|
const isFilled = floatingEqual(betAmount, takerAmount)
|
||||||
|
|
||||||
const newBet: CandidateBet = removeUndefinedProps({
|
const newBet: CandidateBet = removeUndefinedProps({
|
||||||
amount: betAmount,
|
orderAmount: betAmount,
|
||||||
|
amount: takerAmount,
|
||||||
|
shares: takerShares,
|
||||||
limitProb,
|
limitProb,
|
||||||
isFilled,
|
isFilled,
|
||||||
isCancelled: false,
|
isCancelled: false,
|
||||||
fills: takers,
|
fills: takers,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
shares: takerShares,
|
|
||||||
outcome,
|
outcome,
|
||||||
probBefore,
|
probBefore,
|
||||||
probAfter,
|
probAfter,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { Query } from 'firebase-admin/firestore'
|
import { FieldValue, Query } from 'firebase-admin/firestore'
|
||||||
import { sumBy } from 'lodash'
|
import { groupBy, mapValues, sumBy } from 'lodash'
|
||||||
|
|
||||||
import { APIError, newEndpoint, validate } from './api'
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract'
|
import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract'
|
||||||
|
@ -74,6 +74,7 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
bet: LimitBet
|
bet: LimitBet
|
||||||
amount: number
|
amount: number
|
||||||
shares: 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.')
|
throw new APIError(400, 'Bet too large for current liquidity pool.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBalance = user.balance - amount - loanAmount
|
|
||||||
const betDoc = contractDoc.collection('bets').doc()
|
const betDoc = contractDoc.collection('bets').doc()
|
||||||
trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
|
trans.create(betDoc, { id: betDoc.id, userId: user.id, ...newBet })
|
||||||
log('Created new bet document.')
|
log('Created new bet document.')
|
||||||
|
|
||||||
if (makers) {
|
if (makers) {
|
||||||
for (const maker of makers) {
|
const makersByBet = groupBy(makers, (maker) => maker.bet.id)
|
||||||
const { bet, amount, shares } = maker
|
for (const makers of Object.values(makersByBet)) {
|
||||||
const newFill = { amount, shares, matchedBetId: betDoc.id }
|
const bet = makers[0].bet
|
||||||
const fills = [...bet.fills, newFill]
|
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 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.')
|
log('Updated a matched limit bet.')
|
||||||
trans.update(contractDoc.collection('bets').doc(bet.id), {
|
trans.update(contractDoc.collection('bets').doc(bet.id), {
|
||||||
fills,
|
fills,
|
||||||
isFilled,
|
isFilled,
|
||||||
|
amount: totalAmount,
|
||||||
shares: totalShares,
|
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.')
|
log('Updated user balance.')
|
||||||
trans.update(
|
trans.update(
|
||||||
contractDoc,
|
contractDoc,
|
||||||
|
|
|
@ -24,10 +24,7 @@ import { sellShares } from 'web/lib/firebase/api-call'
|
||||||
import { AmountInput, BuyAmountInput } from './amount-input'
|
import { AmountInput, BuyAmountInput } from './amount-input'
|
||||||
import { InfoTooltip } from './info-tooltip'
|
import { InfoTooltip } from './info-tooltip'
|
||||||
import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label'
|
import { BinaryOutcomeLabel, PseudoNumericOutcomeLabel } from './outcome-label'
|
||||||
import {
|
import { getProbability } from 'common/calculate'
|
||||||
calculatePayoutAfterCorrectBet,
|
|
||||||
getProbability,
|
|
||||||
} from 'common/calculate'
|
|
||||||
import { useFocus } from 'web/hooks/use-focus'
|
import { useFocus } from 'web/hooks/use-focus'
|
||||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -28,7 +28,6 @@ export default function BetRow(props: {
|
||||||
contract,
|
contract,
|
||||||
userBets
|
userBets
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -505,7 +505,7 @@ export function ContractBetsTable(props: {
|
||||||
const { contract, className, isYourBets } = props
|
const { contract, className, isYourBets } = props
|
||||||
|
|
||||||
const bets = sortBy(
|
const bets = sortBy(
|
||||||
props.bets.filter((b) => !b.isAnte),
|
props.bets.filter((b) => !b.isAnte && b.amount !== 0),
|
||||||
(bet) => bet.createdTime
|
(bet) => bet.createdTime
|
||||||
).reverse()
|
).reverse()
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,9 @@ export function ContractActivity(props: {
|
||||||
const comments = updatedComments ?? props.comments
|
const comments = updatedComments ?? props.comments
|
||||||
|
|
||||||
const updatedBets = useBets(contract.id)
|
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(
|
const items = getSpecificContractActivityItems(
|
||||||
contract,
|
contract,
|
||||||
bets,
|
bets,
|
||||||
|
|
|
@ -28,7 +28,7 @@ export function LimitBets(props: { bets: LimitBet[]; className?: string }) {
|
||||||
|
|
||||||
function LimitBet(props: { bet: LimitBet }) {
|
function LimitBet(props: { bet: LimitBet }) {
|
||||||
const { bet } = props
|
const { bet } = props
|
||||||
const filledAmount = sumBy(bet.fills, (fill) => fill.amount)
|
const { orderAmount, amount, limitProb, outcome } = bet
|
||||||
const [isCancelling, setIsCancelling] = useState(false)
|
const [isCancelling, setIsCancelling] = useState(false)
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
|
@ -40,11 +40,11 @@ function LimitBet(props: { bet: LimitBet }) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div className="pl-2">
|
<div className="pl-2">
|
||||||
<BinaryOutcomeLabel outcome={bet.outcome as 'YES' | 'NO'} />
|
<BinaryOutcomeLabel outcome={outcome as 'YES' | 'NO'} />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{formatMoney(bet.amount - filledAmount)}</td>
|
<td>{formatMoney(orderAmount - amount)}</td>
|
||||||
<td>{formatPercent(bet.limitProb)}</td>
|
<td>{formatPercent(limitProb)}</td>
|
||||||
<td>
|
<td>
|
||||||
{isCancelling ? (
|
{isCancelling ? (
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
|
|
Loading…
Reference in New Issue
Block a user