Implement selling shares
This commit is contained in:
parent
dd9be0376b
commit
5be2ea8583
|
@ -26,11 +26,7 @@ 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.
|
} & Partial<LimitProps>
|
||||||
// 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 NumericBet = Bet & {
|
export type NumericBet = Bet & {
|
||||||
value: number
|
value: number
|
||||||
|
@ -39,11 +35,16 @@ export type NumericBet = Bet & {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Binary market limit order.
|
// Binary market limit order.
|
||||||
export type LimitBet = Bet & {
|
export type LimitBet = Bet & LimitProps
|
||||||
|
|
||||||
|
type LimitProps = {
|
||||||
orderAmount: number // Amount of limit order.
|
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.
|
||||||
|
// 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[]
|
fills: fill[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +54,9 @@ export type fill = {
|
||||||
amount: number
|
amount: number
|
||||||
shares: number
|
shares: number
|
||||||
timestamp: 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
|
export const MAX_LOAN_PER_CONTRACT = 20
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { sum, groupBy, mapValues, sumBy, partition } from 'lodash'
|
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 { LiquidityProvision } from './liquidity-provision'
|
||||||
|
import { computeFills } from './new-bet'
|
||||||
import { addObjects } from './util/object'
|
import { addObjects } from './util/object'
|
||||||
|
|
||||||
export type CpmmState = {
|
export type CpmmState = {
|
||||||
|
@ -166,119 +168,85 @@ function binarySearch(
|
||||||
return mid
|
return mid
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeK(y: number, n: number, p: number) {
|
function calculateAmountToBuyShares(
|
||||||
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(
|
|
||||||
state: CpmmState,
|
state: CpmmState,
|
||||||
shares: number,
|
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 totalShares = sumBy(takers, (taker) => taker.shares)
|
||||||
const k = computeK(pool.YES, pool.NO, p)
|
return totalShares - shares
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateCpmmSale(
|
export function calculateCpmmSale(
|
||||||
state: CpmmState,
|
state: CpmmState,
|
||||||
shares: number,
|
shares: number,
|
||||||
outcome: string
|
outcome: 'YES' | 'NO',
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
) {
|
) {
|
||||||
if (Math.round(shares) < 0) {
|
if (Math.round(shares) < 0) {
|
||||||
throw new Error('Cannot sell non-positive shares')
|
throw new Error('Cannot sell non-positive shares')
|
||||||
}
|
}
|
||||||
|
|
||||||
const saleValue = calculateCpmmShareValue(
|
const oppositeOutcome = outcome === 'YES' ? 'NO' : 'YES'
|
||||||
|
const buyAmount = calculateAmountToBuyShares(
|
||||||
state,
|
state,
|
||||||
shares,
|
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(
|
// Transform buys of opposite outcome into sells.
|
||||||
// contract,
|
const saleTakers = takers.map((taker) => ({
|
||||||
// rawSaleValue,
|
...taker,
|
||||||
// outcome === 'YES' ? 'NO' : 'YES'
|
// 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 saleValue = -sumBy(saleTakers, (taker) => taker.amount)
|
||||||
const { YES: y, NO: n } = pool
|
|
||||||
|
|
||||||
const { liquidityFee: fee } = fees
|
return {
|
||||||
|
saleValue,
|
||||||
const [newY, newN] =
|
cpmmState,
|
||||||
outcome === 'YES'
|
fees: totalFees,
|
||||||
? [y + shares - saleValue + fee, n - saleValue + fee]
|
makers,
|
||||||
: [y - saleValue + fee, n + shares - saleValue + fee]
|
takers: saleTakers,
|
||||||
|
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const postBetPool = { YES: newY, NO: newN }
|
|
||||||
|
|
||||||
const { newPool, newP } = addCpmmLiquidity(postBetPool, state.p, fee)
|
|
||||||
|
|
||||||
return { saleValue, newPool, newP, fees }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCpmmProbabilityAfterSale(
|
export function getCpmmProbabilityAfterSale(
|
||||||
state: CpmmState,
|
state: CpmmState,
|
||||||
shares: number,
|
shares: number,
|
||||||
outcome: 'YES' | 'NO'
|
outcome: 'YES' | 'NO',
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
) {
|
) {
|
||||||
const { newPool } = calculateCpmmSale(state, shares, outcome)
|
const { cpmmState } = calculateCpmmSale(state, shares, outcome, unfilledBets)
|
||||||
return getCpmmProbability(newPool, state.p)
|
return getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCpmmLiquidity(
|
export function getCpmmLiquidity(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { maxBy } from 'lodash'
|
import { maxBy } from 'lodash'
|
||||||
import { Bet } from './bet'
|
import { Bet, LimitBet } from './bet'
|
||||||
import {
|
import {
|
||||||
calculateCpmmSale,
|
calculateCpmmSale,
|
||||||
getCpmmProbability,
|
getCpmmProbability,
|
||||||
|
@ -74,11 +74,20 @@ export function calculateShares(
|
||||||
: calculateDpmShares(contract.totalShares, bet, betChoice)
|
: 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' &&
|
return contract.mechanism === 'cpmm-1' &&
|
||||||
(contract.outcomeType === 'BINARY' ||
|
(contract.outcomeType === 'BINARY' ||
|
||||||
contract.outcomeType === 'PSEUDO_NUMERIC')
|
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)
|
: calculateDpmSaleAmount(contract, bet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,10 +100,16 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
|
||||||
export function getProbabilityAfterSale(
|
export function getProbabilityAfterSale(
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
outcome: string,
|
outcome: string,
|
||||||
shares: number
|
shares: number,
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
) {
|
) {
|
||||||
return contract.mechanism === 'cpmm-1'
|
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)
|
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -135,10 +135,10 @@ const computeFill = (
|
||||||
return { maker, taker }
|
return { maker, taker }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBinaryCpmmBetInfo = (
|
export const computeFills = (
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
betAmount: number,
|
betAmount: number,
|
||||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
state: CpmmState,
|
||||||
limitProb: number | undefined,
|
limitProb: number | undefined,
|
||||||
unfilledBets: LimitBet[]
|
unfilledBets: LimitBet[]
|
||||||
) => {
|
) => {
|
||||||
|
@ -157,7 +157,7 @@ export const getBinaryCpmmBetInfo = (
|
||||||
}[] = []
|
}[] = []
|
||||||
|
|
||||||
let amount = betAmount
|
let amount = betAmount
|
||||||
let cpmmState = { pool: contract.pool, p: contract.p }
|
let cpmmState = { pool: state.pool, p: state.p }
|
||||||
let totalFees = noFees
|
let totalFees = noFees
|
||||||
|
|
||||||
let i = 0
|
let i = 0
|
||||||
|
@ -185,6 +185,24 @@ export const getBinaryCpmmBetInfo = (
|
||||||
if (floatingEqual(amount, 0)) break
|
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 probBefore = getCpmmProbability(contract.pool, contract.p)
|
||||||
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Bet } from './bet'
|
import { Bet, LimitBet } from './bet'
|
||||||
import {
|
import {
|
||||||
calculateDpmShareValue,
|
calculateDpmShareValue,
|
||||||
deductDpmFees,
|
deductDpmFees,
|
||||||
|
@ -7,6 +7,7 @@ import {
|
||||||
import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm'
|
import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm'
|
||||||
import { CPMMContract, DPMContract } from './contract'
|
import { CPMMContract, DPMContract } from './contract'
|
||||||
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
|
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
|
||||||
|
import { sumBy } from 'lodash'
|
||||||
|
|
||||||
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
|
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
|
||||||
|
|
||||||
|
@ -78,19 +79,24 @@ export const getCpmmSellBetInfo = (
|
||||||
shares: number,
|
shares: number,
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
contract: CPMMContract,
|
contract: CPMMContract,
|
||||||
prevLoanAmount: number
|
prevLoanAmount: number,
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
) => {
|
) => {
|
||||||
const { pool, p } = contract
|
const { pool, p } = contract
|
||||||
|
|
||||||
const { saleValue, newPool, newP, fees } = calculateCpmmSale(
|
const { saleValue, cpmmState, fees, makers, takers } = calculateCpmmSale(
|
||||||
contract,
|
contract,
|
||||||
shares,
|
shares,
|
||||||
outcome
|
outcome,
|
||||||
|
unfilledBets
|
||||||
)
|
)
|
||||||
|
|
||||||
const loanPaid = Math.min(prevLoanAmount, saleValue)
|
const loanPaid = Math.min(prevLoanAmount, saleValue)
|
||||||
const probBefore = getCpmmProbability(pool, p)
|
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(
|
console.log(
|
||||||
'SELL M$',
|
'SELL M$',
|
||||||
|
@ -104,20 +110,26 @@ export const getCpmmSellBetInfo = (
|
||||||
|
|
||||||
const newBet: CandidateBet<Bet> = {
|
const newBet: CandidateBet<Bet> = {
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount: -saleValue,
|
amount: takerAmount,
|
||||||
shares: -shares,
|
shares: takerShares,
|
||||||
outcome,
|
outcome,
|
||||||
probBefore,
|
probBefore,
|
||||||
probAfter,
|
probAfter,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
loanAmount: -loanPaid,
|
loanAmount: -loanPaid,
|
||||||
fees,
|
fees,
|
||||||
|
fills: takers,
|
||||||
|
isFilled: true,
|
||||||
|
isCancelled: false,
|
||||||
|
orderAmount: takerAmount,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newBet,
|
newBet,
|
||||||
newPool,
|
newPool: cpmmState.pool,
|
||||||
newP,
|
newP: cpmmState.p,
|
||||||
fees,
|
fees,
|
||||||
|
makers,
|
||||||
|
takers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { z } from 'zod'
|
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 { groupBy, mapValues, sumBy } from 'lodash'
|
||||||
|
|
||||||
import { APIError, newEndpoint, validate } from './api'
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
|
@ -70,12 +75,7 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
makers,
|
makers,
|
||||||
} = await (async (): Promise<
|
} = await (async (): Promise<
|
||||||
BetInfo & {
|
BetInfo & {
|
||||||
makers?: {
|
makers?: maker[]
|
||||||
bet: LimitBet
|
|
||||||
amount: number
|
|
||||||
shares: number
|
|
||||||
timestamp: number
|
|
||||||
}[]
|
|
||||||
}
|
}
|
||||||
> => {
|
> => {
|
||||||
if (
|
if (
|
||||||
|
@ -83,19 +83,10 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
mechanism == 'cpmm-1'
|
mechanism == 'cpmm-1'
|
||||||
) {
|
) {
|
||||||
const { outcome, limitProb } = validate(binarySchema, req.body)
|
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())
|
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
|
||||||
|
|
||||||
return getBinaryCpmmBetInfo(
|
return getBinaryCpmmBetInfo(
|
||||||
|
@ -134,37 +125,7 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
log('Created new bet document.')
|
log('Created new bet document.')
|
||||||
|
|
||||||
if (makers) {
|
if (makers) {
|
||||||
const makersByBet = groupBy(makers, (maker) => maker.bet.id)
|
updateMakers(makers, betDoc.id, contractDoc, trans)
|
||||||
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) })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trans.update(userDoc, { balance: FieldValue.increment(-newBet.amount) })
|
trans.update(userDoc, { balance: FieldValue.increment(-newBet.amount) })
|
||||||
|
@ -193,3 +154,55 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
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 { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||||
import { getValues } from './utils'
|
import { getValues } from './utils'
|
||||||
import { Bet } from '../../common/bet'
|
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({
|
const bodySchema = z.object({
|
||||||
contractId: z.string(),
|
contractId: z.string(),
|
||||||
|
@ -46,14 +49,22 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
||||||
const outcomeBets = userBets.filter((bet) => bet.outcome == outcome)
|
const outcomeBets = userBets.filter((bet) => bet.outcome == outcome)
|
||||||
const maxShares = sumBy(outcomeBets, (bet) => bet.shares)
|
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.`)
|
throw new APIError(400, `You can only sell up to ${maxShares} shares.`)
|
||||||
|
|
||||||
const { newBet, newPool, newP, fees } = getCpmmSellBetInfo(
|
const soldShares = Math.min(shares, maxShares)
|
||||||
shares,
|
|
||||||
|
const unfilledBetsSnap = await transaction.get(
|
||||||
|
getUnfilledBetsQuery(contractDoc)
|
||||||
|
)
|
||||||
|
const unfilledBets = unfilledBetsSnap.docs.map((doc) => doc.data())
|
||||||
|
|
||||||
|
const { newBet, newPool, newP, fees, makers } = getCpmmSellBetInfo(
|
||||||
|
soldShares,
|
||||||
outcome,
|
outcome,
|
||||||
contract,
|
contract,
|
||||||
prevLoanAmount
|
prevLoanAmount,
|
||||||
|
unfilledBets
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -65,11 +76,17 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
|
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 })
|
updateMakers(makers, newBetDoc.id, contractDoc, transaction)
|
||||||
transaction.create(newBetDoc, { id: newBetDoc.id, userId, ...newBet })
|
|
||||||
|
transaction.update(userDoc, {
|
||||||
|
balance: FieldValue.increment(-newBet.amount),
|
||||||
|
})
|
||||||
|
transaction.create(newBetDoc, {
|
||||||
|
id: newBetDoc.id,
|
||||||
|
userId: user.id,
|
||||||
|
...newBet,
|
||||||
|
})
|
||||||
transaction.update(
|
transaction.update(
|
||||||
contractDoc,
|
contractDoc,
|
||||||
removeUndefinedProps({
|
removeUndefinedProps({
|
||||||
|
|
|
@ -497,19 +497,21 @@ export function SellPanel(props: {
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [wasSubmitted, setWasSubmitted] = useState(false)
|
const [wasSubmitted, setWasSubmitted] = useState(false)
|
||||||
|
|
||||||
|
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||||
|
|
||||||
const betDisabled = isSubmitting || !amount || error
|
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() {
|
async function submitSell() {
|
||||||
if (!user || !amount) return
|
if (!user || !amount) return
|
||||||
|
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
// Sell all shares if remaining shares would be < 1
|
|
||||||
const sellAmount = amount === Math.floor(shares) ? shares : amount
|
|
||||||
|
|
||||||
await sellShares({
|
await sellShares({
|
||||||
shares: sellAmount,
|
shares: sellQuantity,
|
||||||
outcome: sharesOutcome,
|
outcome: sharesOutcome,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
})
|
})
|
||||||
|
@ -534,18 +536,19 @@ export function SellPanel(props: {
|
||||||
outcomeType: contract.outcomeType,
|
outcomeType: contract.outcomeType,
|
||||||
slug: contract.slug,
|
slug: contract.slug,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
shares: sellAmount,
|
shares: sellQuantity,
|
||||||
outcome: sharesOutcome,
|
outcome: sharesOutcome,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialProb = getProbability(contract)
|
const initialProb = getProbability(contract)
|
||||||
const { newPool } = calculateCpmmSale(
|
const { cpmmState, saleValue } = calculateCpmmSale(
|
||||||
contract,
|
contract,
|
||||||
Math.min(amount ?? 0, shares),
|
sellQuantity ?? 0,
|
||||||
sharesOutcome
|
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 openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
const [yesBets, noBets] = partition(
|
const [yesBets, noBets] = partition(
|
||||||
|
@ -557,17 +560,8 @@ export function SellPanel(props: {
|
||||||
sumBy(noBets, (bet) => bet.shares),
|
sumBy(noBets, (bet) => bet.shares),
|
||||||
]
|
]
|
||||||
|
|
||||||
const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
|
||||||
const ownedShares = Math.round(yesShares) || Math.round(noShares)
|
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) => {
|
const onAmountChange = (amount: number | undefined) => {
|
||||||
setAmount(amount)
|
setAmount(amount)
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,8 @@ import { NumericContract } from 'common/contract'
|
||||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { SellSharesModal } from './sell-modal'
|
import { SellSharesModal } from './sell-modal'
|
||||||
|
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||||
|
import { LimitBet } from 'common/bet'
|
||||||
|
|
||||||
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
||||||
type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all'
|
type BetFilter = 'open' | 'sold' | 'closed' | 'resolved' | 'all'
|
||||||
|
@ -531,6 +533,8 @@ export function ContractBetsTable(props: {
|
||||||
const isNumeric = outcomeType === 'NUMERIC'
|
const isNumeric = outcomeType === 'NUMERIC'
|
||||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
|
||||||
|
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('overflow-x-auto', className)}>
|
<div className={clsx('overflow-x-auto', className)}>
|
||||||
{amountRedeemed > 0 && (
|
{amountRedeemed > 0 && (
|
||||||
|
@ -577,6 +581,7 @@ export function ContractBetsTable(props: {
|
||||||
saleBet={salesDict[bet.id]}
|
saleBet={salesDict[bet.id]}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
isYourBet={isYourBets}
|
isYourBet={isYourBets}
|
||||||
|
unfilledBets={unfilledBets}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -590,8 +595,9 @@ function BetRow(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
saleBet?: Bet
|
saleBet?: Bet
|
||||||
isYourBet: boolean
|
isYourBet: boolean
|
||||||
|
unfilledBets: LimitBet[]
|
||||||
}) {
|
}) {
|
||||||
const { bet, saleBet, contract, isYourBet } = props
|
const { bet, saleBet, contract, isYourBet, unfilledBets } = props
|
||||||
const {
|
const {
|
||||||
amount,
|
amount,
|
||||||
outcome,
|
outcome,
|
||||||
|
@ -621,7 +627,7 @@ function BetRow(props: {
|
||||||
formatMoney(
|
formatMoney(
|
||||||
isResolved
|
isResolved
|
||||||
? resolvedPayout(contract, bet)
|
? 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
|
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
|
const profit = saleAmount - bet.amount
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { sellShares } from 'web/lib/firebase/api-call'
|
||||||
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||||
|
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||||
|
|
||||||
const BET_SIZE = 10
|
const BET_SIZE = 10
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
const isCpmm = mechanism === 'cpmm-1'
|
const isCpmm = mechanism === 'cpmm-1'
|
||||||
|
|
||||||
const userBets = useUserContractBets(user.id, contract.id)
|
const userBets = useUserContractBets(user.id, contract.id)
|
||||||
|
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||||
const topAnswer =
|
const topAnswer =
|
||||||
outcomeType === 'FREE_RESPONSE' ? getTopAnswer(contract) : undefined
|
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)
|
const maxSharesSold = BET_SIZE / (sellOutcome === 'YES' ? prob : 1 - prob)
|
||||||
sharesSold = Math.min(oppositeShares, maxSharesSold)
|
sharesSold = Math.min(oppositeShares, maxSharesSold)
|
||||||
|
|
||||||
const { newPool, saleValue } = calculateCpmmSale(
|
const { cpmmState, saleValue } = calculateCpmmSale(
|
||||||
contract,
|
contract,
|
||||||
sharesSold,
|
sharesSold,
|
||||||
sellOutcome
|
sellOutcome,
|
||||||
|
unfilledBets
|
||||||
)
|
)
|
||||||
saleAmount = saleValue
|
saleAmount = saleValue
|
||||||
previewProb = getCpmmProbability(newPool, contract.p)
|
previewProb = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -172,7 +172,6 @@ export function listenForUnfilledBets(
|
||||||
) {
|
) {
|
||||||
const betsQuery = query(
|
const betsQuery = query(
|
||||||
collection(db, 'contracts', contractId, 'bets'),
|
collection(db, 'contracts', contractId, 'bets'),
|
||||||
where('contractId', '==', contractId),
|
|
||||||
where('isFilled', '==', false),
|
where('isFilled', '==', false),
|
||||||
where('isCancelled', '==', false)
|
where('isCancelled', '==', false)
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user