diff --git a/common/bet.ts b/common/bet.ts index 7da4b18c..a3e8e714 100644 --- a/common/bet.ts +++ b/common/bet.ts @@ -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 diff --git a/common/new-bet.ts b/common/new-bet.ts index 29fd421a..11637792 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -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 +} diff --git a/common/payouts.ts b/common/payouts.ts index 5c29d6a9..446b75d7 100644 --- a/common/payouts.ts +++ b/common/payouts.ts @@ -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 })) +} diff --git a/common/sell-bet.ts b/common/sell-bet.ts index cc824386..1b3b133d 100644 --- a/common/sell-bet.ts +++ b/common/sell-bet.ts @@ -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, diff --git a/functions/src/create-answer.ts b/functions/src/create-answer.ts index 5e711e03..f192fc7e 100644 --- a/functions/src/create-answer.ts +++ b/functions/src/create-answer.ts @@ -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( 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, { diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index e37dc12c..4adb1779 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -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, { diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 5da5b272..93d0352e 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -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) )