Implement selling shares
This commit is contained in:
parent
dd9be0376b
commit
5be2ea8583
|
@ -26,11 +26,7 @@ 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?: fill[]
|
||||
}
|
||||
} & Partial<LimitProps>
|
||||
|
||||
export type NumericBet = Bet & {
|
||||
value: number
|
||||
|
@ -39,11 +35,16 @@ export type NumericBet = Bet & {
|
|||
}
|
||||
|
||||
// Binary market limit order.
|
||||
export type LimitBet = Bet & {
|
||||
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[]
|
||||
}
|
||||
|
||||
|
@ -53,6 +54,9 @@ export type fill = {
|
|||
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,7 +1,9 @@
|
|||
import { sum, groupBy, mapValues, sumBy, partition } from 'lodash'
|
||||
import { LimitBet } from './bet'
|
||||
|
||||
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, noFees, PLATFORM_FEE } from './fees'
|
||||
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import { computeFills } from './new-bet'
|
||||
import { addObjects } from './util/object'
|
||||
|
||||
export type CpmmState = {
|
||||
|
@ -166,119 +168,85 @@ function binarySearch(
|
|||
return mid
|
||||
}
|
||||
|
||||
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(
|
||||
function calculateAmountToBuyShares(
|
||||
state: CpmmState,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO'
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
const { pool, p } = state
|
||||
// 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
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
|
||||
// Break once we've reached max precision.
|
||||
if (mid === lowAmount || mid === highAmount) break
|
||||
|
||||
kGuess = sellSharesK(pool.YES, pool.NO, p, shares, outcome, mid)
|
||||
if (kGuess < k) {
|
||||
highAmount = mid
|
||||
} else {
|
||||
lowAmount = mid
|
||||
}
|
||||
}
|
||||
return mid
|
||||
const totalShares = sumBy(takers, (taker) => taker.shares)
|
||||
return totalShares - shares
|
||||
})
|
||||
}
|
||||
|
||||
export function calculateCpmmSale(
|
||||
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 saleValue = calculateCpmmShareValue(
|
||||
const oppositeOutcome = outcome === 'YES' ? 'NO' : 'YES'
|
||||
const buyAmount = calculateAmountToBuyShares(
|
||||
state,
|
||||
shares,
|
||||
outcome as 'YES' | 'NO'
|
||||
oppositeOutcome,
|
||||
unfilledBets
|
||||
)
|
||||
|
||||
const fees = noFees
|
||||
const { cpmmState, makers, takers, totalFees } = computeFills(
|
||||
oppositeOutcome,
|
||||
buyAmount,
|
||||
state,
|
||||
undefined,
|
||||
unfilledBets
|
||||
)
|
||||
|
||||
// const { fees, remainingBet: saleValue } = getCpmmLiquidityFee(
|
||||
// contract,
|
||||
// rawSaleValue,
|
||||
// outcome === 'YES' ? 'NO' : 'YES'
|
||||
// )
|
||||
// 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 { pool } = state
|
||||
const { YES: y, NO: n } = pool
|
||||
const saleValue = -sumBy(saleTakers, (taker) => taker.amount)
|
||||
|
||||
const { liquidityFee: fee } = fees
|
||||
|
||||
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, state.p, fee)
|
||||
|
||||
return { saleValue, newPool, newP, fees }
|
||||
}
|
||||
|
||||
export function getCpmmProbabilityAfterSale(
|
||||
state: CpmmState,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO'
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
const { newPool } = calculateCpmmSale(state, shares, outcome)
|
||||
return getCpmmProbability(newPool, state.p)
|
||||
const { cpmmState } = calculateCpmmSale(state, shares, outcome, unfilledBets)
|
||||
return getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
}
|
||||
|
||||
export function getCpmmLiquidity(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { maxBy } from 'lodash'
|
||||
import { Bet } from './bet'
|
||||
import { Bet, LimitBet } from './bet'
|
||||
import {
|
||||
calculateCpmmSale,
|
||||
getCpmmProbability,
|
||||
|
@ -74,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)
|
||||
}
|
||||
|
||||
|
@ -91,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)
|
||||
}
|
||||
|
||||
|
|
|
@ -135,10 +135,10 @@ const computeFill = (
|
|||
return { maker, taker }
|
||||
}
|
||||
|
||||
export const getBinaryCpmmBetInfo = (
|
||||
export const computeFills = (
|
||||
outcome: 'YES' | 'NO',
|
||||
betAmount: number,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
state: CpmmState,
|
||||
limitProb: number | undefined,
|
||||
unfilledBets: LimitBet[]
|
||||
) => {
|
||||
|
@ -157,7 +157,7 @@ export const getBinaryCpmmBetInfo = (
|
|||
}[] = []
|
||||
|
||||
let amount = betAmount
|
||||
let cpmmState = { pool: contract.pool, p: contract.p }
|
||||
let cpmmState = { pool: state.pool, p: state.p }
|
||||
let totalFees = noFees
|
||||
|
||||
let i = 0
|
||||
|
@ -185,6 +185,24 @@ export const getBinaryCpmmBetInfo = (
|
|||
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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { z } from 'zod'
|
||||
import { FieldValue, Query } from 'firebase-admin/firestore'
|
||||
import {
|
||||
DocumentReference,
|
||||
FieldValue,
|
||||
Query,
|
||||
Transaction,
|
||||
} from 'firebase-admin/firestore'
|
||||
import { groupBy, mapValues, sumBy } from 'lodash'
|
||||
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
|
@ -70,12 +75,7 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
|||
makers,
|
||||
} = await (async (): Promise<
|
||||
BetInfo & {
|
||||
makers?: {
|
||||
bet: LimitBet
|
||||
amount: number
|
||||
shares: number
|
||||
timestamp: number
|
||||
}[]
|
||||
makers?: maker[]
|
||||
}
|
||||
> => {
|
||||
if (
|
||||
|
@ -83,19 +83,10 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
|||
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' ? '<=' : '>=',
|
||||
boundedLimitProb
|
||||
) as Query<LimitBet>
|
||||
|
||||
const unfilledBetsSnap = await trans.get(unfilledBetsQuery)
|
||||
const unfilledBetsSnap = await trans.get(
|
||||
getUnfilledBetsQuery(contractDoc)
|
||||
)
|
||||
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
|
||||
|
||||
return getBinaryCpmmBetInfo(
|
||||
|
@ -134,37 +125,7 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
|||
log('Created new bet document.')
|
||||
|
||||
if (makers) {
|
||||
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 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.
|
||||
// TODO: Check if users would go negative from fills and cancel those bets.
|
||||
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) })
|
||||
}
|
||||
updateMakers(makers, betDoc.id, contractDoc, trans)
|
||||
}
|
||||
|
||||
trans.update(userDoc, { balance: FieldValue.increment(-newBet.amount) })
|
||||
|
@ -193,3 +154,55 @@ 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.
|
||||
// TODO: Check if users would go negative from fills and cancel those bets.
|
||||
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({
|
||||
|
|
|
@ -497,19 +497,21 @@ export function SellPanel(props: {
|
|||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [wasSubmitted, setWasSubmitted] = useState(false)
|
||||
|
||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||
|
||||
const betDisabled = isSubmitting || !amount || error
|
||||
|
||||
// Sell all shares if remaining shares would be < 1
|
||||
const sellQuantity = amount === Math.floor(shares) ? shares : amount
|
||||
|
||||
async function submitSell() {
|
||||
if (!user || !amount) return
|
||||
|
||||
setError(undefined)
|
||||
setIsSubmitting(true)
|
||||
|
||||
// Sell all shares if remaining shares would be < 1
|
||||
const sellAmount = amount === Math.floor(shares) ? shares : amount
|
||||
|
||||
await sellShares({
|
||||
shares: sellAmount,
|
||||
shares: sellQuantity,
|
||||
outcome: sharesOutcome,
|
||||
contractId: contract.id,
|
||||
})
|
||||
|
@ -534,18 +536,19 @@ export function SellPanel(props: {
|
|||
outcomeType: contract.outcomeType,
|
||||
slug: contract.slug,
|
||||
contractId: contract.id,
|
||||
shares: sellAmount,
|
||||
shares: sellQuantity,
|
||||
outcome: sharesOutcome,
|
||||
})
|
||||
}
|
||||
|
||||
const initialProb = getProbability(contract)
|
||||
const { newPool } = calculateCpmmSale(
|
||||
const { cpmmState, saleValue } = calculateCpmmSale(
|
||||
contract,
|
||||
Math.min(amount ?? 0, shares),
|
||||
sharesOutcome
|
||||
sellQuantity ?? 0,
|
||||
sharesOutcome,
|
||||
unfilledBets
|
||||
)
|
||||
const resultProb = getCpmmProbability(newPool, contract.p)
|
||||
const resultProb = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
|
||||
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||
const [yesBets, noBets] = partition(
|
||||
|
@ -557,17 +560,8 @@ export function SellPanel(props: {
|
|||
sumBy(noBets, (bet) => bet.shares),
|
||||
]
|
||||
|
||||
const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
||||
const ownedShares = Math.round(yesShares) || Math.round(noShares)
|
||||
|
||||
const sharesSold = Math.min(amount ?? 0, ownedShares)
|
||||
|
||||
const { saleValue } = calculateCpmmSale(
|
||||
contract,
|
||||
sharesSold,
|
||||
sellOutcome as 'YES' | 'NO'
|
||||
)
|
||||
|
||||
const onAmountChange = (amount: number | undefined) => {
|
||||
setAmount(amount)
|
||||
|
||||
|
|
|
@ -44,6 +44,8 @@ import { NumericContract } from 'common/contract'
|
|||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { SellSharesModal } from './sell-modal'
|
||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||
import { LimitBet } from 'common/bet'
|
||||
|
||||
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
||||
type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all'
|
||||
|
@ -531,6 +533,8 @@ export function ContractBetsTable(props: {
|
|||
const isNumeric = outcomeType === 'NUMERIC'
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||
|
||||
return (
|
||||
<div className={clsx('overflow-x-auto', className)}>
|
||||
{amountRedeemed > 0 && (
|
||||
|
@ -577,6 +581,7 @@ export function ContractBetsTable(props: {
|
|||
saleBet={salesDict[bet.id]}
|
||||
contract={contract}
|
||||
isYourBet={isYourBets}
|
||||
unfilledBets={unfilledBets}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
@ -590,8 +595,9 @@ function BetRow(props: {
|
|||
contract: Contract
|
||||
saleBet?: Bet
|
||||
isYourBet: boolean
|
||||
unfilledBets: LimitBet[]
|
||||
}) {
|
||||
const { bet, saleBet, contract, isYourBet } = props
|
||||
const { bet, saleBet, contract, isYourBet, unfilledBets } = props
|
||||
const {
|
||||
amount,
|
||||
outcome,
|
||||
|
@ -621,7 +627,7 @@ function BetRow(props: {
|
|||
formatMoney(
|
||||
isResolved
|
||||
? resolvedPayout(contract, bet)
|
||||
: calculateSaleAmount(contract, bet)
|
||||
: calculateSaleAmount(contract, bet, unfilledBets)
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -681,9 +687,16 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
|||
outcome === 'NO' ? 'YES' : outcome
|
||||
)
|
||||
|
||||
const outcomeProb = getProbabilityAfterSale(contract, outcome, shares)
|
||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||
|
||||
const saleAmount = calculateSaleAmount(contract, bet)
|
||||
const outcomeProb = getProbabilityAfterSale(
|
||||
contract,
|
||||
outcome,
|
||||
shares,
|
||||
unfilledBets
|
||||
)
|
||||
|
||||
const saleAmount = calculateSaleAmount(contract, bet, unfilledBets)
|
||||
const profit = saleAmount - bet.amount
|
||||
|
||||
return (
|
||||
|
|
|
@ -27,6 +27,7 @@ import { sellShares } from 'web/lib/firebase/api-call'
|
|||
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||
|
||||
const BET_SIZE = 10
|
||||
|
||||
|
@ -36,6 +37,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
const isCpmm = mechanism === 'cpmm-1'
|
||||
|
||||
const userBets = useUserContractBets(user.id, contract.id)
|
||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||
const topAnswer =
|
||||
outcomeType === 'FREE_RESPONSE' ? getTopAnswer(contract) : undefined
|
||||
|
||||
|
@ -85,13 +87,14 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
|||
const maxSharesSold = BET_SIZE / (sellOutcome === 'YES' ? prob : 1 - prob)
|
||||
sharesSold = Math.min(oppositeShares, maxSharesSold)
|
||||
|
||||
const { newPool, saleValue } = calculateCpmmSale(
|
||||
const { cpmmState, saleValue } = calculateCpmmSale(
|
||||
contract,
|
||||
sharesSold,
|
||||
sellOutcome
|
||||
sellOutcome,
|
||||
unfilledBets
|
||||
)
|
||||
saleAmount = saleValue
|
||||
previewProb = getCpmmProbability(newPool, contract.p)
|
||||
previewProb = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -172,7 +172,6 @@ export function listenForUnfilledBets(
|
|||
) {
|
||||
const betsQuery = query(
|
||||
collection(db, 'contracts', contractId, 'bets'),
|
||||
where('contractId', '==', contractId),
|
||||
where('isFilled', '==', false),
|
||||
where('isCancelled', '==', false)
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue
Block a user