diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 13f5068d..d6ce9c6a 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -10,6 +10,11 @@ import { Spacer } from './layout/spacer' import { YesNoSelector } from './yes-no-selector' import { formatMoney, formatPercent } from '../lib/util/format' import { Title } from './title' +import { + getProbability, + getDpmWeight, + getProbabilityAfterBet, +} from '../lib/calculation/contract' export function BetPanel(props: { contract: Contract; className?: string }) { const { contract, className } = props @@ -79,8 +84,12 @@ export function BetPanel(props: { contract: Contract; className?: string }) { const betDisabled = isSubmitting || !betAmount || error - const initialProb = getProbability(contract.pot, betChoice) - const resultProb = getProbability(contract.pot, betChoice, betAmount) + const initialProb = getProbability(contract.pot) + const resultProb = getProbabilityAfterBet( + contract.pot, + betChoice, + betAmount ?? 0 + ) const dpmWeight = getDpmWeight(contract.pot, betAmount ?? 0, betChoice) const estimatedWinnings = Math.floor((betAmount ?? 0) + dpmWeight) @@ -166,29 +175,3 @@ export function BetPanel(props: { contract: Contract; className?: string }) { const functions = getFunctions() export const placeBet = httpsCallable(functions, 'placeBet') - -const getProbability = ( - pot: { YES: number; NO: number }, - outcome: 'YES' | 'NO', - bet = 0 -) => { - const [yesPot, noPot] = [ - pot.YES + (outcome === 'YES' ? bet : 0), - pot.NO + (outcome === 'NO' ? bet : 0), - ] - const numerator = Math.pow(yesPot, 2) - const denominator = Math.pow(yesPot, 2) + Math.pow(noPot, 2) - return numerator / denominator -} - -const getDpmWeight = ( - pot: { YES: number; NO: number }, - bet: number, - betChoice: 'YES' | 'NO' -) => { - const [yesPot, noPot] = [pot.YES, pot.NO] - - return betChoice === 'YES' - ? (bet * Math.pow(noPot, 2)) / (Math.pow(yesPot, 2) + bet * yesPot) - : (bet * Math.pow(yesPot, 2)) / (Math.pow(noPot, 2) + bet * noPot) -} diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 403427eb..d62cd19e 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -7,8 +7,10 @@ import { Bet } from '../lib/firebase/bets' import { User } from '../lib/firebase/users' import { formatMoney, formatPercent } from '../lib/util/format' import { Col } from './layout/col' -import { ContractDetails } from './contracts-list' import { Spacer } from './layout/spacer' +import { Contract } from '../lib/firebase/contracts' +import { Row } from './layout/row' +import { calculateWinnings, currentValue } from '../lib/calculation/contract' export function BetsList(props: { user: User }) { const { user } = props @@ -25,7 +27,7 @@ export function BetsList(props: { user: User }) { return ( {Object.keys(contractBets).map((contractId) => ( - + const { resolution } = contract + + const betsTotal = _.sumBy(bets, (bet) => bet.amount) + + const betsValue = _.sumBy(bets, (bet) => currentValue(contract, bet)) + + const yesWinnings = _.sumBy(bets, (bet) => + calculateWinnings(contract, bet, 'YES') + ) + const noWinnings = _.sumBy(bets, (bet) => + calculateWinnings(contract, bet, 'NO') + ) + return (
-

+

{contract.question} -

- +
+ + +
By {contract.creatorName}
+ {resolution &&
} + {resolution === 'YES' && ( +
Resolved YES
+ )} + {resolution === 'NO' && ( +
Resolved NO
+ )} + {resolution === 'CANCEL' && ( +
Resolved CANCEL
+ )} +
- -
- - - - - - - - - - - - {bets.map((bet) => ( - - ))} - -
OutcomeAmountProbabilityEstimated payoffDate
-
+ + + + + +
Total bets
+
{formatMoney(betsTotal)}
+ + {resolution ? ( + <> + +
Winnings
+
{formatMoney(yesWinnings)}
+ + + ) : ( + <> + +
Current value
+
{formatMoney(betsValue)}
+ + +
If YES
+
{formatMoney(yesWinnings)}
+ + +
If NO
+
{formatMoney(noWinnings)}
+ + + )} +
+ + + +
) } -function BetRow(props: { bet: Bet }) { - const { bet } = props +function ContractBetsTable(props: { contract: Contract; bets: Bet[] }) { + const { contract, bets } = props + + return ( +
+ + + + + + + + + + + + + {bets.map((bet) => ( + + ))} + +
OutcomeBetProbabilityDateEst. max payoutProfit/loss
+
+ ) +} + +function BetRow(props: { bet: Bet; contract: Contract }) { + const { bet, contract } = props const { amount, outcome, createdTime, probBefore, probAfter, dpmWeight } = bet return ( @@ -85,8 +156,9 @@ function BetRow(props: { bet: Bet }) { {formatPercent(probBefore)} → {formatPercent(probAfter)} - {formatMoney(amount + dpmWeight)} {dayjs(createdTime).format('MMM D, H:mma')} + {formatMoney(amount + dpmWeight)} + {formatMoney(currentValue(contract, bet) - amount)} ) } diff --git a/web/components/contracts-list.tsx b/web/components/contracts-list.tsx index 9c98692c..e8fd0463 100644 --- a/web/components/contracts-list.tsx +++ b/web/components/contracts-list.tsx @@ -18,7 +18,7 @@ export function ContractDetails(props: { contract: Contract }) { {resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}
-
{formatMoney(volume)} pot
+
{formatMoney(volume)} bet
) } diff --git a/web/lib/calculation/contract.ts b/web/lib/calculation/contract.ts new file mode 100644 index 00000000..37a80b6a --- /dev/null +++ b/web/lib/calculation/contract.ts @@ -0,0 +1,66 @@ +import { Bet } from '../firebase/bets' +import { Contract } from '../firebase/contracts' + +export function getProbability(pot: { YES: number; NO: number }) { + const [yesPot, noPot] = [pot.YES, pot.NO] + const numerator = Math.pow(yesPot, 2) + const denominator = Math.pow(yesPot, 2) + Math.pow(noPot, 2) + return numerator / denominator +} + +export function getProbabilityAfterBet( + pot: { YES: number; NO: number }, + outcome: 'YES' | 'NO', + bet: number +) { + const [YES, NO] = [ + pot.YES + (outcome === 'YES' ? bet : 0), + pot.NO + (outcome === 'NO' ? bet : 0), + ] + return getProbability({ YES, NO }) +} + +export function getDpmWeight( + pot: { YES: number; NO: number }, + bet: number, + betChoice: 'YES' | 'NO' +) { + const [yesPot, noPot] = [pot.YES, pot.NO] + + return betChoice === 'YES' + ? (bet * Math.pow(noPot, 2)) / (Math.pow(yesPot, 2) + bet * yesPot) + : (bet * Math.pow(yesPot, 2)) / (Math.pow(noPot, 2) + bet * noPot) +} + +export function calculateWinnings( + contract: Contract, + bet: Bet, + outcome: 'YES' | 'NO' | 'CANCEL' +) { + let { dpmWeights, pot } = contract + const { amount, outcome: betOutcome, dpmWeight } = bet + + if (outcome === 'CANCEL') return amount + + if (!dpmWeights) { + // Fake data if not set. + dpmWeights = { YES: 100, NO: 100 } + } + // Fake data if not set. + if (!pot) pot = { YES: 100, NO: 100 } + + return betOutcome === outcome + ? 0.98 * + (dpmWeight / dpmWeights[outcome]) * + pot[outcome === 'YES' ? 'NO' : 'YES'] + + amount + : 0 +} + +export function currentValue(contract: Contract, bet: Bet) { + const prob = getProbability(contract.pot) + const yesWinnings = calculateWinnings(contract, bet, 'YES') + const noWinnings = calculateWinnings(contract, bet, 'NO') + + return prob * yesWinnings + (1 - prob) * noWinnings +} diff --git a/web/lib/service/create-contract.ts b/web/lib/service/create-contract.ts index 56ee335f..e33c1a3b 100644 --- a/web/lib/service/create-contract.ts +++ b/web/lib/service/create-contract.ts @@ -31,7 +31,6 @@ export async function createContract( seedAmounts: { YES: seedYes, NO: seedNo }, pot: { YES: seedYes, NO: seedNo }, dpmWeights: { YES: 0, NO: 0 }, - isResolved: false, // TODO: Set create time to Firestore timestamp