diff --git a/packages/finance/amms/maniswap-v1.1.test.ts b/packages/finance/amms/maniswap-v1.1.test.ts index 24591412..217ef885 100644 --- a/packages/finance/amms/maniswap-v1.1.test.ts +++ b/packages/finance/amms/maniswap-v1.1.test.ts @@ -9,55 +9,83 @@ jest.mock('@play-money/markets/lib/getMarketOption', () => ({ getMarketOption: j describe('maniswap-v1.1', () => { describe('trade', () => { - it('should return correct amount for buying YES', async () => { - // Current probability = 0.75 - const amount = await trade({ - amount: new Decimal(50), - targetShare: new Decimal(100), - shares: [new Decimal(100), new Decimal(300)], - isBuy: true, - }) - - expect(amount).toBeCloseToDecimal(64.29) - }) - - it('should return correct amount for buying NO', async () => { - // Current probability = 0.75 - const amount = await trade({ - amount: new Decimal(50), - targetShare: new Decimal(300), - shares: [new Decimal(100), new Decimal(300)], - isBuy: true, - }) - - expect(amount).toBeCloseToDecimal(150) - }) - - // This is the inverse of the test for buying YES - it('should return correct amount for selling YES', async () => { - // Current probability ~= 0.80 - const amount = await trade({ - amount: new Decimal(64.29), - targetShare: new Decimal(85.71), - shares: [new Decimal(85.71), new Decimal(350)], - isBuy: false, - }) + test.each([ + { targetShare: 100, shares: [100, 300], expected: 64.29 }, + { targetShare: 100, shares: [100, 400, 400, 400], expected: 79.76 }, + { targetShare: 100, shares: [100, 400, 400, 400, 400, 400, 400, 400, 400], expected: 111.02 }, + ])( + 'should return $expected for buying high targetShare: $targetShare of shares: $shares', + async ({ targetShare, shares, expected }) => { + // Current probability = 0.75 + const amount = await trade({ + amount: new Decimal(50), + targetShare: new Decimal(targetShare), + shares: shares.map((share) => new Decimal(share)), + isBuy: true, + }) + + expect(amount).toBeCloseToDecimal(expected) + } + ) - expect(amount).toBeCloseToDecimal(50) - }) + test.each([ + { targetShare: 300, shares: [100, 300], expected: 150 }, + { targetShare: 400, shares: [100, 400, 400, 400], expected: 239.3 }, + { targetShare: 400, shares: [100, 400, 400, 400, 400, 400, 400, 400, 400], expected: 333.07 }, + ])( + 'should return $expected for buying low targetShare: $targetShare of shares: $shares', + async ({ targetShare, shares, expected }) => { + // Current probability = 0.75 + const amount = await trade({ + amount: new Decimal(50), + targetShare: new Decimal(targetShare), + shares: shares.map((share) => new Decimal(share)), + isBuy: true, + }) + + expect(amount).toBeCloseToDecimal(expected) + } + ) - // This is the inverse of the test for buying NO - it('should return correct amount for selling NO', async () => { - // Current probability ~= 0.57 - const amount = await trade({ - amount: new Decimal(150), - targetShare: new Decimal(200), - shares: [new Decimal(150), new Decimal(200)], - isBuy: false, - }) + // This is the inverse of the test for buying high + test.each([ + { targetShare: 85.71, shares: [85.71, 350], expected: 50 }, + { targetShare: 85.71, shares: [85.71, 400, 400, 400], expected: 36.13 }, + { targetShare: 85.71, shares: [85.71, 400, 400, 400, 400, 400, 400, 400, 400], expected: 20.21 }, + ])( + 'should return $expected for selling high targetShare: $targetShare of shares: $shares', + async ({ targetShare, shares, expected }) => { + // Current probability = 0.75 + const amount = await trade({ + amount: new Decimal(64.29), + targetShare: new Decimal(targetShare), + shares: shares.map((share) => new Decimal(share)), + isBuy: false, + }) + + expect(amount).toBeCloseToDecimal(expected) + } + ) - expect(amount).toBeCloseToDecimal(50) - }) + // This is the inverse of the test for buying low + test.each([ + { targetShare: 200, shares: [150, 200], expected: 50 }, + { targetShare: 200, shares: [150, 200, 450, 450], expected: 36.58 }, + { targetShare: 200, shares: [150, 200, 450, 450, 450, 450, 450, 450, 450], expected: 21.45 }, + ])( + 'should return $expected for selling low targetShare: $targetShare of shares: $shares', + async ({ targetShare, shares, expected }) => { + // Current probability = 0.75 + const amount = await trade({ + amount: new Decimal(150), + targetShare: new Decimal(targetShare), + shares: shares.map((share) => new Decimal(share)), + isBuy: false, + }) + + expect(amount).toBeCloseToDecimal(expected) + } + ) }) describe('quote', () => { @@ -185,8 +213,21 @@ describe('maniswap-v1.1', () => { shares: [new Decimal(200), new Decimal(200), new Decimal(200)], }) - expect(result.probability).toBeCloseToDecimal(0.48) - expect(result.shares).toBeCloseToDecimal(72.22) + expect(result.probability).toBeCloseToDecimal(0.59) + expect(result.shares).toBeCloseToDecimal(122) + }) + + // Inverse of previous buy + it('should return correct quote for selling option in multiple choice', async () => { + const result = await quote({ + amount: new Decimal(116.66), + probability: new Decimal(0.01), + targetShare: new Decimal(128), + shares: [new Decimal(128), new Decimal(250), new Decimal(250)], + }) + + expect(result.probability).toBeCloseToDecimal(0.34) + expect(result.shares).toBeCloseToDecimal(48.2) }) }) @@ -206,7 +247,93 @@ describe('maniswap-v1.1', () => { ], }) - expect(result).toEqual([expect.closeToDecimal(50), expect.closeToDecimal(50)]) + expect(result).toEqual([ + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.5) }, + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.5) }, + ]) + }) + + it('should correctly add liquidity to an imbalanced market', async () => { + const result = await addLiquidity({ + amount: new Decimal(50), + options: [ + { + shares: new Decimal(100), + liquidityProbability: new Decimal(0.3), + }, + { + shares: new Decimal(300), + liquidityProbability: new Decimal(0.7), + }, + ], + }) + + expect(result).toEqual([ + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.25) }, + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.75) }, + ]) + }) + + it('should correctly add liquidity to a balanced 4 option market', async () => { + const result = await addLiquidity({ + amount: new Decimal(50), + options: [ + { + shares: new Decimal(1000), + liquidityProbability: new Decimal(0.25), + }, + { + shares: new Decimal(1000), + liquidityProbability: new Decimal(0.25), + }, + { + shares: new Decimal(1000), + liquidityProbability: new Decimal(0.25), + }, + { + shares: new Decimal(1000), + liquidityProbability: new Decimal(0.25), + }, + ], + }) + + expect(result).toEqual([ + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.25) }, + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.25) }, + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.25) }, + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.25) }, + ]) + }) + + it('should correctly add liquidity to an imbalanced 4 option market', async () => { + const result = await addLiquidity({ + amount: new Decimal(50), + options: [ + { + shares: new Decimal(100), + liquidityProbability: new Decimal(0.25), + }, + { + shares: new Decimal(300), + liquidityProbability: new Decimal(0.25), + }, + { + shares: new Decimal(300), + liquidityProbability: new Decimal(0.25), + }, + { + shares: new Decimal(300), + liquidityProbability: new Decimal(0.25), + }, + ], + }) + + expect(result).toEqual([ + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.205) }, + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.265) }, + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.265) }, + { newShares: expect.closeToDecimal(50), liquidityProbability: expect.closeToDecimal(0.265) }, + ]) }) }) diff --git a/packages/finance/amms/maniswap-v1.1.ts b/packages/finance/amms/maniswap-v1.1.ts index 43df31db..4856af0b 100644 --- a/packages/finance/amms/maniswap-v1.1.ts +++ b/packages/finance/amms/maniswap-v1.1.ts @@ -7,38 +7,40 @@ import Decimal from 'decimal.js' For more information, see https://manifoldmarkets.notion.site/Maniswap-ce406e1e897d417cbd491071ea8a0c39 */ -function calculateTrade({ + +function calculateSellDifference({ amount, - targetShare, - totalShares, - isBuy, + targetShareIndex, + shares, + amountReturning, }: { amount: Decimal - targetShare: Decimal - totalShares: Decimal - isBuy: boolean + targetShareIndex: number + shares: Array + amountReturning: Decimal }) { - if (isBuy) { - return amount.times(amount.add(totalShares)).div(amount.add(totalShares.sub(targetShare))) - } - - return totalShares - .add(amount) - .sub(Decimal.sqrt(totalShares.add(amount).pow(2).sub(totalShares.sub(targetShare).times(4).times(amount)))) - .times(0.5) + const sharesProduct = multiplyShares(shares) + return sharesProduct.sub( + multiplyShares( + shares.map((share, i) => + i === targetShareIndex ? share.sub(amountReturning).plus(amount) : share.sub(amountReturning) + ) + ) + ) } function calculateProbabilityCost({ probability, targetShare, - totalShares, + shares, isBuy, }: { probability: Decimal targetShare: Decimal - totalShares: Decimal + shares: Array isBuy: boolean }): Decimal { + const totalShares = sumShares(shares) const totalOppositeShares = totalShares.sub(targetShare) const factor = isBuy ? probability.neg().div(probability.sub(1)) : probability.sub(1).neg().div(probability) @@ -58,20 +60,52 @@ function findShareIndex(shares: Array, targetShare: Decimal): number { return shares.findIndex((share) => share.eq(targetShare)) } -function sumShares(shares: Array) { - return shares.reduce((sum, share) => sum.add(share), new Decimal(0)) +function sumShares(shares: Array) { + return shares.reduce((sum, share) => sum.add(share), new Decimal(0)) +} + +function multiplyShares(shares: Array) { + return shares.reduce((sum, share) => sum.times(share), new Decimal(1)) } export function calculateProbability({ index, shares }: { index: number; shares: Array }): Decimal { const indexShares = shares[index] - - // Calculate the sum of the shares of each index - const sum = shares.reduce((sum, share) => sum.plus(share), new Decimal(0)) + const sum = sumShares(shares) // The probability for the given index is one minus the share count at the index times the number of dimensions divided by the sum of all shares - const probability = new Decimal(1).sub(new Decimal(indexShares).mul(shares.length - 1).div(sum)) + return new Decimal(1).sub(new Decimal(indexShares).mul(shares.length - 1).div(sum)) +} - return probability +function binarySearch( + evaluate: (mid: Decimal) => Decimal, + low: Decimal, + high: Decimal, + tolerance: Decimal = new Decimal('0.0001'), + maxIterations: number = 100 +): Decimal { + let iterationCount = 0 + while (high.minus(low).gt(tolerance)) { + if (iterationCount >= maxIterations) { + throw new Error('Failed to converge. Max iterations reached.') + } + + const mid = low.plus(high).div(2) + const result = evaluate(mid) + + if (result.abs().lte(tolerance)) { + // If we're very close to zero, favor the upper bound + return high + } else if (result.lessThan(0)) { + low = mid + } else { + high = mid + } + + iterationCount++ + } + + // Return the upper bound to ensure largest possible value + return high } export function trade({ @@ -85,8 +119,24 @@ export function trade({ shares: Array isBuy: boolean }) { - const totalShares = sumShares(shares) - return calculateTrade({ amount, targetShare, totalShares, isBuy }) + const targetShareIndex = findShareIndex(shares, targetShare) + + // (num shares of buying) + (amount of currency buying) - (product of all share nums)/(product of for each option, sum of option and the amount of currency buying) + // When buying x: returnAmount = x + a - xyz/((y+a)(z+a)) + if (isBuy) { + const sharesProduct = multiplyShares(shares) + const sharesWithoutTarget = shares.filter((_, i) => i !== targetShareIndex) + const sharesProductAdded = multiplyShares(sharesWithoutTarget.map((share) => share.plus(amount))) + + return targetShare.plus(amount).sub(sharesProduct.div(sharesProductAdded)) + } + + // When selling n dimensions doesn't have a closed form, so we use binary search to find the amount to sell + return binarySearch( + (amountReturning: Decimal) => calculateSellDifference({ amount, targetShareIndex, shares, amountReturning }), + new Decimal(0), + amount + ) } export async function quote({ @@ -101,14 +151,13 @@ export async function quote({ shares: Array }): Promise<{ probability: Decimal; shares: Decimal; cost: Decimal }> { const targetIndex = findShareIndex(shares, targetShare) - const totalShares = sumShares(shares) const currentProbability = calculateProbability({ index: targetIndex, shares }) const isBuy = currentProbability.lt(probability) - let costToHitProbability = calculateProbabilityCost({ probability, targetShare, totalShares, isBuy }) + let costToHitProbability = calculateProbabilityCost({ probability, targetShare, shares, isBuy }) const cost = Decimal.min(costToHitProbability, amount) - const returnedShares = calculateTrade({ amount: cost, targetShare, totalShares, isBuy }) + const returnedShares = trade({ amount: cost, targetShare, shares, isBuy }) const updatedShares = shares.map((share, i) => i === targetIndex ? share.sub(returnedShares).add(cost) : isBuy ? share.add(cost) : share.sub(returnedShares) @@ -130,40 +179,15 @@ export function addLiquidity({ }: { amount: Decimal options: Array<{ shares: Decimal; liquidityProbability: Decimal }> -}): Array { - // const totalShares = options.reduce((sum, option) => sum.add(option.shares), new Decimal(0)) - // const newShares: Decimal[] = [] - - // for (const option of options) { - // const { shares, liquidityProbability } = option - // const currentProbability = shares.div(totalShares) - - // const p = calculateNewP(currentProbability, liquidityProbability, amount, shares, totalShares) - // const newOptionShares = calculateNewShares(p, amount, shares, totalShares) - - // newShares.push(newOptionShares) - // } - - return [amount, amount] +}): Array<{ newShares: Decimal; liquidityProbability: Decimal }> { + const denominator = options.reduce((sum, option) => { + return sum.add(option.liquidityProbability.mul(option.shares).div(option.shares.add(amount))) + }, new Decimal(0)) + + return options.map(({ shares, liquidityProbability }) => { + return { + newShares: amount, + liquidityProbability: liquidityProbability.mul(shares).div(shares.add(amount)).div(denominator), + } + }) } - -// function calculateNewP( -// currentProbability: Decimal, -// liquidityProbability: Decimal, -// amount: Decimal, -// shares: Decimal, -// totalShares: Decimal -// ): Decimal { -// // This is a simplified approximation. You may need to use numerical methods for more accuracy. -// const weight = amount.div(totalShares.add(amount)) -// return currentProbability.mul(Decimal.sub(1, weight)).add(liquidityProbability.mul(weight)) -// } - -// function calculateNewShares(p: Decimal, amount: Decimal, shares: Decimal, totalShares: Decimal): Decimal { -// const n = totalShares.sub(shares) -// const newShares = Decimal.pow(shares.add(amount), p) -// .mul(Decimal.pow(n.add(amount), Decimal.sub(1, p))) -// .sub(Decimal.pow(shares, p).mul(Decimal.pow(n, Decimal.sub(1, p)))) - -// return newShares -// } diff --git a/packages/markets/lib/updateMarketPositionValues.test.ts b/packages/markets/lib/updateMarketPositionValues.test.ts index 51d2ec0b..4e5dec9a 100644 --- a/packages/markets/lib/updateMarketPositionValues.test.ts +++ b/packages/markets/lib/updateMarketPositionValues.test.ts @@ -154,7 +154,7 @@ describe('updateMarketPositionValues', () => { optionId: 'option2', cost: new Decimal(40), quantity: new Decimal(75), - value: new Decimal(9.0147), + value: new Decimal(9.0148), }), ])