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:
		
							parent
							
								
									c372a0af9d
								
							
						
					
					
						commit
						659f848bec
					
				|  | @ -4,6 +4,7 @@ export type Bet = { | ||||||
|   contractId: string |   contractId: string | ||||||
| 
 | 
 | ||||||
|   amount: number // bet size; negative if SELL bet
 |   amount: number // bet size; negative if SELL bet
 | ||||||
|  |   loanAmount?: number | ||||||
|   outcome: string |   outcome: string | ||||||
|   shares: number // dynamic parimutuel pool weight; negative if SELL bet
 |   shares: number // dynamic parimutuel pool weight; negative if SELL bet
 | ||||||
| 
 | 
 | ||||||
|  | @ -21,3 +22,5 @@ export type Bet = { | ||||||
| 
 | 
 | ||||||
|   createdTime: number |   createdTime: number | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export const MAX_LOAN_PER_CONTRACT = 20 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { Bet } from './bet' | import * as _ from 'lodash' | ||||||
|  | import { Bet, MAX_LOAN_PER_CONTRACT } from './bet' | ||||||
| import { | import { | ||||||
|   calculateShares, |   calculateShares, | ||||||
|   getProbability, |   getProbability, | ||||||
|  | @ -11,6 +12,7 @@ export const getNewBinaryBetInfo = ( | ||||||
|   user: User, |   user: User, | ||||||
|   outcome: 'YES' | 'NO', |   outcome: 'YES' | 'NO', | ||||||
|   amount: number, |   amount: number, | ||||||
|  |   loanAmount: number, | ||||||
|   contract: Contract, |   contract: Contract, | ||||||
|   newBetId: string |   newBetId: string | ||||||
| ) => { | ) => { | ||||||
|  | @ -45,6 +47,7 @@ export const getNewBinaryBetInfo = ( | ||||||
|     userId: user.id, |     userId: user.id, | ||||||
|     contractId: contract.id, |     contractId: contract.id, | ||||||
|     amount, |     amount, | ||||||
|  |     loanAmount, | ||||||
|     shares, |     shares, | ||||||
|     outcome, |     outcome, | ||||||
|     probBefore, |     probBefore, | ||||||
|  | @ -52,7 +55,7 @@ export const getNewBinaryBetInfo = ( | ||||||
|     createdTime: Date.now(), |     createdTime: Date.now(), | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const newBalance = user.balance - amount |   const newBalance = user.balance - (amount - loanAmount) | ||||||
| 
 | 
 | ||||||
|   return { newBet, newPool, newTotalShares, newTotalBets, newBalance } |   return { newBet, newPool, newTotalShares, newTotalBets, newBalance } | ||||||
| } | } | ||||||
|  | @ -61,6 +64,7 @@ export const getNewMultiBetInfo = ( | ||||||
|   user: User, |   user: User, | ||||||
|   outcome: string, |   outcome: string, | ||||||
|   amount: number, |   amount: number, | ||||||
|  |   loanAmount: number, | ||||||
|   contract: Contract, |   contract: Contract, | ||||||
|   newBetId: string |   newBetId: string | ||||||
| ) => { | ) => { | ||||||
|  | @ -85,6 +89,7 @@ export const getNewMultiBetInfo = ( | ||||||
|     userId: user.id, |     userId: user.id, | ||||||
|     contractId: contract.id, |     contractId: contract.id, | ||||||
|     amount, |     amount, | ||||||
|  |     loanAmount, | ||||||
|     shares, |     shares, | ||||||
|     outcome, |     outcome, | ||||||
|     probBefore, |     probBefore, | ||||||
|  | @ -92,7 +97,16 @@ export const getNewMultiBetInfo = ( | ||||||
|     createdTime: Date.now(), |     createdTime: Date.now(), | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const newBalance = user.balance - amount |   const newBalance = user.balance - (amount - loanAmount) | ||||||
| 
 | 
 | ||||||
|   return { newBet, newPool, newTotalShares, newTotalBets, newBalance } |   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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -161,3 +161,12 @@ export const getPayoutsMultiOutcome = ( | ||||||
|     .map(({ userId, payout }) => ({ userId, payout })) |     .map(({ userId, payout }) => ({ userId, payout })) | ||||||
|     .concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
 |     .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 })) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ export const getSellBetInfo = ( | ||||||
|   newBetId: string |   newBetId: string | ||||||
| ) => { | ) => { | ||||||
|   const { pool, totalShares, totalBets } = contract |   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) |   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 { |   return { | ||||||
|     newBet, |     newBet, | ||||||
|  |  | ||||||
|  | @ -3,10 +3,11 @@ import * as admin from 'firebase-admin' | ||||||
| 
 | 
 | ||||||
| import { Contract } from '../../common/contract' | import { Contract } from '../../common/contract' | ||||||
| import { User } from '../../common/user' | import { User } from '../../common/user' | ||||||
| import { getNewMultiBetInfo } from '../../common/new-bet' | import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet' | ||||||
| import { Answer } from '../../common/answer' | import { Answer } from '../../common/answer' | ||||||
| import { getContract, getValues } from './utils' | import { getContract, getValues } from './utils' | ||||||
| import { sendNewAnswerEmail } from './emails' | import { sendNewAnswerEmail } from './emails' | ||||||
|  | import { Bet } from '../../common/bet' | ||||||
| 
 | 
 | ||||||
| export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( | export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( | ||||||
|   async ( |   async ( | ||||||
|  | @ -55,6 +56,11 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( | ||||||
|       if (closeTime && Date.now() > closeTime) |       if (closeTime && Date.now() > closeTime) | ||||||
|         return { status: 'error', message: 'Trading is closed' } |         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>( |       const [lastAnswer] = await getValues<Answer>( | ||||||
|         firestore |         firestore | ||||||
|           .collection(`contracts/${contractId}/answers`) |           .collection(`contracts/${contractId}/answers`) | ||||||
|  | @ -92,8 +98,17 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( | ||||||
|         .collection(`contracts/${contractId}/bets`) |         .collection(`contracts/${contractId}/bets`) | ||||||
|         .doc() |         .doc() | ||||||
| 
 | 
 | ||||||
|  |       const loanAmount = getLoanAmount(yourBets, amount) | ||||||
|  | 
 | ||||||
|       const { newBet, newPool, newTotalShares, newTotalBets, newBalance } = |       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.create(newBetDoc, newBet) | ||||||
|       transaction.update(contractDoc, { |       transaction.update(contractDoc, { | ||||||
|  |  | ||||||
|  | @ -3,7 +3,13 @@ import * as admin from 'firebase-admin' | ||||||
| 
 | 
 | ||||||
| import { Contract } from '../../common/contract' | import { Contract } from '../../common/contract' | ||||||
| import { User } from '../../common/user' | 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( | export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( | ||||||
|   async ( |   async ( | ||||||
|  | @ -46,6 +52,11 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( | ||||||
|       if (closeTime && Date.now() > closeTime) |       if (closeTime && Date.now() > closeTime) | ||||||
|         return { status: 'error', message: 'Trading is closed' } |         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') { |       if (outcomeType === 'FREE_RESPONSE') { | ||||||
|         const answerSnap = await transaction.get( |         const answerSnap = await transaction.get( | ||||||
|           contractDoc.collection('answers').doc(outcome) |           contractDoc.collection('answers').doc(outcome) | ||||||
|  | @ -58,16 +69,26 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( | ||||||
|         .collection(`contracts/${contractId}/bets`) |         .collection(`contracts/${contractId}/bets`) | ||||||
|         .doc() |         .doc() | ||||||
| 
 | 
 | ||||||
|  |       const loanAmount = getLoanAmount(yourBets, amount) | ||||||
|  | 
 | ||||||
|       const { newBet, newPool, newTotalShares, newTotalBets, newBalance } = |       const { newBet, newPool, newTotalShares, newTotalBets, newBalance } = | ||||||
|         outcomeType === 'BINARY' |         outcomeType === 'BINARY' | ||||||
|           ? getNewBinaryBetInfo( |           ? getNewBinaryBetInfo( | ||||||
|               user, |               user, | ||||||
|               outcome as 'YES' | 'NO', |               outcome as 'YES' | 'NO', | ||||||
|               amount, |               amount, | ||||||
|  |               loanAmount, | ||||||
|  |               contract, | ||||||
|  |               newBetDoc.id | ||||||
|  |             ) | ||||||
|  |           : getNewMultiBetInfo( | ||||||
|  |               user, | ||||||
|  |               outcome, | ||||||
|  |               amount, | ||||||
|  |               loanAmount, | ||||||
|               contract, |               contract, | ||||||
|               newBetDoc.id |               newBetDoc.id | ||||||
|             ) |             ) | ||||||
|           : getNewMultiBetInfo(user, outcome, amount, contract, newBetDoc.id) |  | ||||||
| 
 | 
 | ||||||
|       transaction.create(newBetDoc, newBet) |       transaction.create(newBetDoc, newBet) | ||||||
|       transaction.update(contractDoc, { |       transaction.update(contractDoc, { | ||||||
|  |  | ||||||
|  | @ -7,7 +7,11 @@ import { User } from '../../common/user' | ||||||
| import { Bet } from '../../common/bet' | import { Bet } from '../../common/bet' | ||||||
| import { getUser, payUser } from './utils' | import { getUser, payUser } from './utils' | ||||||
| import { sendMarketResolutionEmail } from './emails' | import { sendMarketResolutionEmail } from './emails' | ||||||
| import { getPayouts, getPayoutsMultiOutcome } from '../../common/payouts' | import { | ||||||
|  |   getLoanPayouts, | ||||||
|  |   getPayouts, | ||||||
|  |   getPayoutsMultiOutcome, | ||||||
|  | } from '../../common/payouts' | ||||||
| import { removeUndefinedProps } from '../../common/util/object' | import { removeUndefinedProps } from '../../common/util/object' | ||||||
| 
 | 
 | ||||||
| export const resolveMarket = functions | export const resolveMarket = functions | ||||||
|  | @ -99,9 +103,14 @@ export const resolveMarket = functions | ||||||
|           ? getPayoutsMultiOutcome(resolutions, contract, openBets) |           ? getPayoutsMultiOutcome(resolutions, contract, openBets) | ||||||
|           : getPayouts(outcome, contract, openBets, resolutionProbability) |           : getPayouts(outcome, contract, openBets, resolutionProbability) | ||||||
| 
 | 
 | ||||||
|  |       const loanPayouts = getLoanPayouts(openBets) | ||||||
|  | 
 | ||||||
|       console.log('payouts:', payouts) |       console.log('payouts:', payouts) | ||||||
| 
 | 
 | ||||||
|       const groups = _.groupBy(payouts, (payout) => payout.userId) |       const groups = _.groupBy( | ||||||
|  |         [...payouts, ...loanPayouts], | ||||||
|  |         (payout) => payout.userId | ||||||
|  |       ) | ||||||
|       const userPayouts = _.mapValues(groups, (group) => |       const userPayouts = _.mapValues(groups, (group) => | ||||||
|         _.sumBy(group, (g) => g.payout) |         _.sumBy(group, (g) => g.payout) | ||||||
|       ) |       ) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user