Loan backend: Add loanAmount field to Bet, manage loans up to max loan amount per market -- buy, sell, and resolve.

This commit is contained in:
James Grugett 2022-03-01 16:23:00 -08:00
parent c372a0af9d
commit 659f848bec
7 changed files with 82 additions and 11 deletions

View File

@ -4,6 +4,7 @@ export type Bet = {
contractId: string
amount: number // bet size; negative if SELL bet
loanAmount?: number
outcome: string
shares: number // dynamic parimutuel pool weight; negative if SELL bet
@ -21,3 +22,5 @@ export type Bet = {
createdTime: number
}
export const MAX_LOAN_PER_CONTRACT = 20

View File

@ -1,4 +1,5 @@
import { Bet } from './bet'
import * as _ from 'lodash'
import { Bet, MAX_LOAN_PER_CONTRACT } from './bet'
import {
calculateShares,
getProbability,
@ -11,6 +12,7 @@ export const getNewBinaryBetInfo = (
user: User,
outcome: 'YES' | 'NO',
amount: number,
loanAmount: number,
contract: Contract,
newBetId: string
) => {
@ -45,6 +47,7 @@ export const getNewBinaryBetInfo = (
userId: user.id,
contractId: contract.id,
amount,
loanAmount,
shares,
outcome,
probBefore,
@ -52,7 +55,7 @@ export const getNewBinaryBetInfo = (
createdTime: Date.now(),
}
const newBalance = user.balance - amount
const newBalance = user.balance - (amount - loanAmount)
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
}
@ -61,6 +64,7 @@ export const getNewMultiBetInfo = (
user: User,
outcome: string,
amount: number,
loanAmount: number,
contract: Contract,
newBetId: string
) => {
@ -85,6 +89,7 @@ export const getNewMultiBetInfo = (
userId: user.id,
contractId: contract.id,
amount,
loanAmount,
shares,
outcome,
probBefore,
@ -92,7 +97,16 @@ export const getNewMultiBetInfo = (
createdTime: Date.now(),
}
const newBalance = user.balance - amount
const newBalance = user.balance - (amount - loanAmount)
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
}
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
const prevLoanAmount = _.sumBy(yourBets, (bet) => bet.loanAmount ?? 0)
const loanAmount = Math.min(
newBetAmount,
MAX_LOAN_PER_CONTRACT - prevLoanAmount
)
return loanAmount
}

View File

@ -161,3 +161,12 @@ export const getPayoutsMultiOutcome = (
.map(({ userId, payout }) => ({ userId, payout }))
.concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
}
export const getLoanPayouts = (bets: Bet[]) => {
const betsWithLoans = bets.filter((bet) => bet.loanAmount)
const betsByUser = _.groupBy(betsWithLoans, (bet) => bet.userId)
const loansByUser = _.mapValues(betsByUser, (bets) =>
_.sumBy(bets, (bet) => -(bet.loanAmount ?? 0))
)
return _.toPairs(loansByUser).map(([userId, payout]) => ({ userId, payout }))
}

View File

@ -11,7 +11,7 @@ export const getSellBetInfo = (
newBetId: string
) => {
const { pool, totalShares, totalBets } = contract
const { id: betId, amount, shares, outcome } = bet
const { id: betId, amount, shares, outcome, loanAmount } = bet
const adjShareValue = calculateShareValue(contract, bet)
@ -57,7 +57,7 @@ export const getSellBetInfo = (
},
}
const newBalance = user.balance + saleAmount
const newBalance = user.balance + saleAmount - (loanAmount ?? 0)
return {
newBet,

View File

@ -3,10 +3,11 @@ import * as admin from 'firebase-admin'
import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { getNewMultiBetInfo } from '../../common/new-bet'
import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet'
import { Answer } from '../../common/answer'
import { getContract, getValues } from './utils'
import { sendNewAnswerEmail } from './emails'
import { Bet } from '../../common/bet'
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
async (
@ -55,6 +56,11 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' }
const yourBetsSnap = await transaction.get(
contractDoc.collection('bets').where('userId', '==', userId)
)
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
const [lastAnswer] = await getValues<Answer>(
firestore
.collection(`contracts/${contractId}/answers`)
@ -92,8 +98,17 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
.collection(`contracts/${contractId}/bets`)
.doc()
const loanAmount = getLoanAmount(yourBets, amount)
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
getNewMultiBetInfo(user, answerId, amount, contract, newBetDoc.id)
getNewMultiBetInfo(
user,
answerId,
amount,
loanAmount,
contract,
newBetDoc.id
)
transaction.create(newBetDoc, newBet)
transaction.update(contractDoc, {

View File

@ -3,7 +3,13 @@ import * as admin from 'firebase-admin'
import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { getNewBinaryBetInfo, getNewMultiBetInfo } from '../../common/new-bet'
import {
getLoanAmount,
getNewBinaryBetInfo,
getNewMultiBetInfo,
} from '../../common/new-bet'
import { Bet } from '../../common/bet'
import { getValues } from './utils'
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
async (
@ -46,6 +52,11 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' }
const yourBetsSnap = await transaction.get(
contractDoc.collection('bets').where('userId', '==', userId)
)
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
if (outcomeType === 'FREE_RESPONSE') {
const answerSnap = await transaction.get(
contractDoc.collection('answers').doc(outcome)
@ -58,16 +69,26 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
.collection(`contracts/${contractId}/bets`)
.doc()
const loanAmount = getLoanAmount(yourBets, amount)
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
outcomeType === 'BINARY'
? getNewBinaryBetInfo(
user,
outcome as 'YES' | 'NO',
amount,
loanAmount,
contract,
newBetDoc.id
)
: getNewMultiBetInfo(
user,
outcome,
amount,
loanAmount,
contract,
newBetDoc.id
)
: getNewMultiBetInfo(user, outcome, amount, contract, newBetDoc.id)
transaction.create(newBetDoc, newBet)
transaction.update(contractDoc, {

View File

@ -7,7 +7,11 @@ import { User } from '../../common/user'
import { Bet } from '../../common/bet'
import { getUser, payUser } from './utils'
import { sendMarketResolutionEmail } from './emails'
import { getPayouts, getPayoutsMultiOutcome } from '../../common/payouts'
import {
getLoanPayouts,
getPayouts,
getPayoutsMultiOutcome,
} from '../../common/payouts'
import { removeUndefinedProps } from '../../common/util/object'
export const resolveMarket = functions
@ -99,9 +103,14 @@ export const resolveMarket = functions
? getPayoutsMultiOutcome(resolutions, contract, openBets)
: getPayouts(outcome, contract, openBets, resolutionProbability)
const loanPayouts = getLoanPayouts(openBets)
console.log('payouts:', payouts)
const groups = _.groupBy(payouts, (payout) => payout.userId)
const groups = _.groupBy(
[...payouts, ...loanPayouts],
(payout) => payout.userId
)
const userPayouts = _.mapValues(groups, (group) =>
_.sumBy(group, (g) => g.payout)
)