diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts
index 164551bd..4862703f 100644
--- a/functions/src/resolve-market.ts
+++ b/functions/src/resolve-market.ts
@@ -26,7 +26,7 @@ export const resolveMarket = functions
const { outcome, contractId } = data
- if (!['YES', 'NO', 'CANCEL'].includes(outcome))
+ if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome))
return { status: 'error', message: 'Invalid outcome' }
const contractDoc = firestore.doc(`contracts/${contractId}`)
@@ -155,7 +155,6 @@ const getStandardPayouts = (
const shareDifferenceSum = _.sumBy(winningBets, (b) => b.shares - b.amount)
const winningsPool = truePool - betSum
- const fees = PLATFORM_FEE + CREATOR_FEE
const winnerPayouts = winningBets.map((bet) => ({
userId: bet.userId,
@@ -173,11 +172,53 @@ const getStandardPayouts = (
const getMktPayouts = (truePool: number, contract: Contract, bets: Bet[]) => {
const p =
contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2)
- console.log('Resolved MKT at p=', p)
+ console.log('Resolved MKT at p=', p, 'pool: $M', truePool)
+
+ const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES')
+
+ const weightedBetTotal =
+ p * _.sumBy(yesBets, (b) => b.amount) +
+ (1 - p) * _.sumBy(noBets, (b) => b.amount)
+
+ if (weightedBetTotal >= truePool) {
+ return bets.map((bet) => ({
+ userId: bet.userId,
+ payout:
+ (((bet.outcome === 'YES' ? p : 1 - p) * bet.amount) /
+ weightedBetTotal) *
+ truePool,
+ }))
+ }
+
+ const winningsPool = truePool - weightedBetTotal
+
+ const weightedShareTotal =
+ p * _.sumBy(yesBets, (b) => b.shares - b.amount) +
+ (1 - p) * _.sumBy(noBets, (b) => b.shares - b.amount)
+
+ const yesPayouts = yesBets.map((bet) => ({
+ userId: bet.userId,
+ payout:
+ (1 - fees) *
+ (p * bet.amount +
+ ((p * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool),
+ }))
+
+ const noPayouts = noBets.map((bet) => ({
+ userId: bet.userId,
+ payout:
+ (1 - fees) *
+ ((1 - p) * bet.amount +
+ (((1 - p) * (bet.shares - bet.amount)) / weightedShareTotal) *
+ winningsPool),
+ }))
+
+ const creatorPayout = CREATOR_FEE * truePool
return [
- ...getStandardPayouts('YES', p * truePool, contract, bets),
- ...getStandardPayouts('NO', (1 - p) * truePool, contract, bets),
+ ...yesPayouts,
+ ...noPayouts,
+ { userId: contract.creatorId, payout: creatorPayout },
]
}
@@ -192,3 +233,5 @@ const payUser = ([userId, payout]: [string, number]) => {
transaction.update(userDoc, { balance: newUserBalance })
})
}
+
+const fees = PLATFORM_FEE + CREATOR_FEE
diff --git a/functions/src/scripts/recalculate.ts b/functions/src/scripts/recalculate.ts
new file mode 100644
index 00000000..103152c9
--- /dev/null
+++ b/functions/src/scripts/recalculate.ts
@@ -0,0 +1,61 @@
+import * as admin from 'firebase-admin'
+import * as _ from 'lodash'
+import { Bet } from '../types/bet'
+import { Contract } from '../types/contract'
+
+type DocRef = admin.firestore.DocumentReference
+
+// Generate your own private key, and set the path below:
+// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
+const serviceAccount = require('../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
+
+admin.initializeApp({
+ credential: admin.credential.cert(serviceAccount),
+})
+const firestore = admin.firestore()
+
+async function recalculateContract(contractRef: DocRef, contract: Contract) {
+ const bets = await contractRef
+ .collection('bets')
+ .get()
+ .then((snap) => snap.docs.map((bet) => bet.data() as Bet))
+
+ const openBets = bets.filter((b) => !b.isSold && !b.sale)
+
+ const totalShares = {
+ YES: _.sumBy(openBets, (bet) => (bet.outcome === 'YES' ? bet.shares : 0)),
+ NO: _.sumBy(openBets, (bet) => (bet.outcome === 'NO' ? bet.shares : 0)),
+ }
+
+ const totalBets = {
+ YES: _.sumBy(openBets, (bet) => (bet.outcome === 'YES' ? bet.amount : 0)),
+ NO: _.sumBy(openBets, (bet) => (bet.outcome === 'NO' ? bet.amount : 0)),
+ }
+
+ await contractRef.update({ totalShares, totalBets })
+
+ console.log(
+ 'calculating totals for "',
+ contract.question,
+ '" total bets:',
+ totalBets
+ )
+ console.log()
+}
+
+async function migrateContracts() {
+ console.log('Recalculating contract info')
+
+ const snapshot = await firestore.collection('contracts').get()
+ const contracts = snapshot.docs.map((doc) => doc.data() as Contract)
+
+ console.log('Loaded', contracts.length, 'contracts')
+
+ for (const contract of contracts) {
+ const contractRef = firestore.doc(`contracts/${contract.id}`)
+
+ await recalculateContract(contractRef, contract)
+ }
+}
+
+if (require.main === module) migrateContracts().then(() => process.exit())
diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx
index 18c74563..29d51346 100644
--- a/web/components/bets-list.tsx
+++ b/web/components/bets-list.tsx
@@ -191,7 +191,7 @@ export function MyBetsSummary(props: {
const betsTotal = _.sumBy(excludeSales, (bet) => bet.amount)
const betsPayout = resolution
- ? _.sumBy(bets, (bet) => resolvedPayout(contract, bet))
+ ? _.sumBy(excludeSales, (bet) => resolvedPayout(contract, bet))
: 0
const yesWinnings = _.sumBy(excludeSales, (bet) =>
@@ -357,11 +357,12 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
)
}
-function OutcomeLabel(props: { outcome: 'YES' | 'NO' | 'CANCEL' }) {
+function OutcomeLabel(props: { outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' }) {
const { outcome } = props
if (outcome === 'YES') return
if (outcome === 'NO') return
+ if (outcome === 'MKT') return
return
}
@@ -376,3 +377,7 @@ function NoLabel() {
function CancelLabel() {
return N/A
}
+
+function MarketLabel() {
+ return MKT
+}
diff --git a/web/components/contract-overview.tsx b/web/components/contract-overview.tsx
index aa8dfe6c..ec2fc4cb 100644
--- a/web/components/contract-overview.tsx
+++ b/web/components/contract-overview.tsx
@@ -98,6 +98,7 @@ export const ContractOverview = (props: {
const resolutionColor = {
YES: 'text-primary',
NO: 'text-red-400',
+ MKT: 'text-blue-400',
CANCEL: 'text-yellow-400',
'': '', // Empty if unresolved
}[contract.resolution || '']
diff --git a/web/components/contracts-list.tsx b/web/components/contracts-list.tsx
index 07c052b0..dc83bafb 100644
--- a/web/components/contracts-list.tsx
+++ b/web/components/contracts-list.tsx
@@ -44,6 +44,7 @@ function ContractCard(props: { contract: Contract }) {
const resolutionColor = {
YES: 'text-primary',
NO: 'text-red-400',
+ MKT: 'text-blue-400',
CANCEL: 'text-yellow-400',
'': '', // Empty if unresolved
}[contract.resolution || '']
@@ -51,6 +52,7 @@ function ContractCard(props: { contract: Contract }) {
const resolutionText = {
YES: 'YES',
NO: 'NO',
+ MKT: 'MKT',
CANCEL: 'N/A',
'': '',
}[contract.resolution || '']
diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx
index 7f9f74c6..d5ccbfd5 100644
--- a/web/components/resolution-panel.tsx
+++ b/web/components/resolution-panel.tsx
@@ -20,7 +20,9 @@ export function ResolutionPanel(props: {
}) {
const { contract, className } = props
- const [outcome, setOutcome] = useState<'YES' | 'NO' | 'CANCEL' | undefined>()
+ const [outcome, setOutcome] = useState<
+ 'YES' | 'NO' | 'MKT' | 'CANCEL' | undefined
+ >()
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState(undefined)
@@ -48,6 +50,8 @@ export function ResolutionPanel(props: {
? 'bg-red-400 hover:bg-red-500'
: outcome === 'CANCEL'
? 'bg-yellow-400 hover:bg-yellow-500'
+ : outcome === 'MKT'
+ ? 'bg-blue-400 hover:bg-blue-500'
: 'btn-disabled'
return (
@@ -74,6 +78,11 @@ export function ResolutionPanel(props: {
<>Winnings will be paid out to NO bettors. You earn 1% of the pool.>
) : outcome === 'CANCEL' ? (
<>The pool will be returned to traders with no fees.>
+ ) : outcome === 'MKT' ? (
+ <>
+ Traders will be paid out at the current implied probability. You
+ earn 1% of the pool.
+ >
) : (
<>Resolving this market will immediately pay out traders.>
)}
diff --git a/web/components/yes-no-selector.tsx b/web/components/yes-no-selector.tsx
index 391db469..528b199d 100644
--- a/web/components/yes-no-selector.tsx
+++ b/web/components/yes-no-selector.tsx
@@ -1,5 +1,6 @@
import clsx from 'clsx'
import React from 'react'
+import { Col } from './layout/col'
import { Row } from './layout/row'
export function YesNoSelector(props: {
@@ -29,48 +30,60 @@ export function YesNoSelector(props: {
}
export function YesNoCancelSelector(props: {
- selected: 'YES' | 'NO' | 'CANCEL' | undefined
- onSelect: (selected: 'YES' | 'NO' | 'CANCEL') => void
+ selected: 'YES' | 'NO' | 'MKT' | 'CANCEL' | undefined
+ onSelect: (selected: 'YES' | 'NO' | 'MKT' | 'CANCEL') => void
className?: string
btnClassName?: string
}) {
const { selected, onSelect, className } = props
- const btnClassName = clsx('px-6', props.btnClassName)
+ const btnClassName = clsx('px-6 flex-1', props.btnClassName)
return (
-
-
+
+
+
-
+
+
-
-
+
+
+
+
+
+
)
}
function Button(props: {
className?: string
onClick?: () => void
- color: 'green' | 'red' | 'yellow' | 'gray'
+ color: 'green' | 'red' | 'blue' | 'yellow' | 'gray'
children?: any
}) {
const { className, onClick, children, color } = props
@@ -83,6 +96,7 @@ function Button(props: {
color === 'green' && 'btn-primary',
color === 'red' && 'bg-red-400 hover:bg-red-500',
color === 'yellow' && 'bg-yellow-400 hover:bg-yellow-500',
+ color === 'blue' && 'bg-blue-400 hover:bg-blue-500',
color === 'gray' && 'text-gray-700 bg-gray-300 hover:bg-gray-400',
className
)}
diff --git a/web/lib/calculate.ts b/web/lib/calculate.ts
index 642473df..b265f228 100644
--- a/web/lib/calculate.ts
+++ b/web/lib/calculate.ts
@@ -37,11 +37,13 @@ export function calculateShares(
export function calculatePayout(
contract: Contract,
bet: Bet,
- outcome: 'YES' | 'NO' | 'CANCEL'
+ outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
) {
const { amount, outcome: betOutcome, shares } = bet
if (outcome === 'CANCEL') return amount
+ if (outcome === 'MKT') return calculateMktPayout(contract, bet)
+
if (betOutcome !== outcome) return 0
const { totalShares, totalBets } = contract
@@ -60,6 +62,34 @@ export function calculatePayout(
return (1 - fees) * (amount + ((shares - amount) / total) * winningsPool)
}
+function calculateMktPayout(contract: Contract, bet: Bet) {
+ const p =
+ contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2)
+ const weightedTotal =
+ p * contract.totalBets.YES + (1 - p) * contract.totalBets.NO
+
+ const startPool = contract.startPool.YES + contract.startPool.NO
+ const truePool = contract.pool.YES + contract.pool.NO - startPool
+
+ const betP = bet.outcome === 'YES' ? p : 1 - p
+
+ if (weightedTotal >= truePool) {
+ return ((betP * bet.amount) / weightedTotal) * truePool
+ }
+
+ const winningsPool = truePool - weightedTotal
+
+ const weightedShareTotal =
+ p * (contract.totalShares.YES - contract.totalBets.YES) +
+ (1 - p) * (contract.totalShares.NO - contract.totalBets.NO)
+
+ return (
+ (1 - fees) *
+ (betP * bet.amount +
+ ((betP * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool)
+ )
+}
+
export function resolvedPayout(contract: Contract, bet: Bet) {
if (contract.resolution)
return calculatePayout(contract, bet, contract.resolution)