From 4b09640e60f2668de82190add2c19bcaecee6abb Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Tue, 7 Jun 2022 08:37:00 -0700 Subject: [PATCH 01/14] Create a test interface for Uniswap v3 --- common/calculate-swap3.ts | 95 ++++++++++++++++++++++ web/pages/swap.tsx | 166 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 common/calculate-swap3.ts create mode 100644 web/pages/swap.tsx diff --git a/common/calculate-swap3.ts b/common/calculate-swap3.ts new file mode 100644 index 00000000..fc552e2b --- /dev/null +++ b/common/calculate-swap3.ts @@ -0,0 +1,95 @@ +type Swap3LiquidityProvision = { + // TODO: Record who added this stuff? + + // Not sure if this is needed; maybe YES and NO left + // amount: number // M$ quantity + + // For now, only support YES and NO outcome tokens + // TODO: replace with Outcome + // Hm, is this... + // 1. Number of shares left in this particular pool? + // 2. Fixed at injection time? + pool: { YES: number; NO: number } + + // Uniswap uses 0.01, 0.003, 0.0005. Let's stick with 0.003 for now. + // fee: number + + // Min/max is expressed as a odds ratio of cost of YES to cost of NO + // E.g. ratio of 1 = 1:1 = 50%; ratio of 3 = 3:1 = 75% + // minRatio: number + // maxRatio: number + minTick: number + // minTick = loq_sqrt_1.0001(sqrtRatio) + // sqrt(1.0001)^(minTick) = sqrtRatio + // minRatio = 1.0001^minTick + // e.g. minTick = 20k => 7.3883 + maxTick: number +} + +// From https://uniswap.org/whitepaper-v3.pdf +export type Swap3Pool = { + // id: string + // userId: string + // contractId: string + // createdTime: number + + // 6.2 Global State + liquidity: number // = sqrt(NY) + sqrtRatio: number // = sqrt(N / Y); N = # NO shares in pool + // So N = liquidity * sqrtRatio; Y = liquidity / sqrtRatio + + tick: number + // Stored as optimization. equal to floor(log_sqrt_1.0001(sqrtRatio)) + // TODO add fees? +} + +export function getSwap3Probability(pool: Swap3Pool) { + // Probability is given by N / (N + Y) + // const N = pool.liquidity * pool.sqrtRatio + // const Y = pool.liquidity / pool.sqrtRatio + // return N / (N + Y) + + // To check: this should be equal to toProb(pool.tick)? + return toProb(pool.tick) +} + +function calculatePurchase( + pool: Swap3Pool, + amount: number, + outcome: 'YES' | 'NO' +) { + const shares = 10 + const newPool = {} +} + +export function calculateLPCost( + curTick: number, + minTick: number, + maxTick: number, + deltaL: number +) { + const upperTick = Math.min(maxTick, Math.max(minTick, curTick)) + const costN = toRatio(upperTick) ** 0.5 - toRatio(minTick) ** 0.5 + + const lowerTick = Math.max(minTick, Math.min(maxTick, curTick)) + const costY = 1 / toRatio(lowerTick) ** 0.5 - 1 / toRatio(maxTick) ** 0.5 + + return { + requiredN: deltaL * costN, + requiredY: deltaL * costY, + } +} + +function toRatio(tick: number) { + return 1.0001 ** tick +} + +function toProb(tick: number) { + const ratio = toRatio(tick) + return ratio / (ratio + 1) +} + +export function fromProb(prob: number) { + const ratio = prob / (1 - prob) + return Math.log(ratio) / Math.log(1.0001) +} diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx new file mode 100644 index 00000000..9a22b11d --- /dev/null +++ b/web/pages/swap.tsx @@ -0,0 +1,166 @@ +import { + calculateLPCost, + fromProb, + getSwap3Probability, + Swap3Pool, +} from 'common/calculate-swap3' +import { formatPercent } from 'common/util/format' +import { useState } from 'react' +import { Col } from 'web/components/layout/col' +import { Row } from 'web/components/layout/row' + +const users = { + alice: { + M: 100, + YES: 0, + NO: 0, + }, + bob: { + M: 200, + YES: 0, + NO: 0, + }, + kipply: { + M: 300, + YES: 0, + NO: 0, + }, +} + +function BalanceTable() { + /* Display all users current M, YES, and NO in a table */ + return ( + + + + + + + + + + + {Object.keys(users).map((user) => ( + + + + + + + ))} + +
UserMYESNO
{user}{users[user].M}{users[user].YES}{users[user].NO}
+ ) +} + +/* Show the values in pool */ +function PoolTable(props: { pool: Swap3Pool }) { + const { pool } = props + return ( + +
+ + {pool.liquidity} +
+
+ + {pool.sqrtRatio} +
+
+ + {pool.tick} +
+
+ + {pool.liquidity * pool.sqrtRatio} +
+
+ + {pool.liquidity / pool.sqrtRatio} +
+
+ + {formatPercent(getSwap3Probability(pool))} +
+
+ ) +} + +// Stored as optimization. equal to floor(log_sqrt_1.0001(sqrtRatio)) +function tick(sqrtRatio: number) { + return Math.floor(Math.log(sqrtRatio) / Math.log(1.0001)) +} + +export default function Swap() { + const [pool, setPool] = useState({ + liquidity: 100, + sqrtRatio: 2, + tick: tick(0.1), + }) + + const [minTick, setMinTick] = useState(0) + const [maxTick, setMaxTick] = useState(0) + + const { requiredN, requiredY } = calculateLPCost( + pool.tick, + minTick, + maxTick, + 100 // deltaL + ) + + return ( + + + + + setPool((p) => ({ + ...p, + tick: inputPercentToTick(e), + })) + } + /> + + + Alice: Add liquidity + + setMinTick(inputPercentToTick(e))} + /> + Min Tick: {minTick} + setMaxTick(inputPercentToTick(e))} + /> + Max Tick: {maxTick} + +
Y required: {requiredY}
+
N required: {requiredN}
{' '} +
+ + + + + Bob: Buy Tokens + {/* */} + + + + + + + + ) +} + +function inputPercentToTick(event: React.ChangeEvent) { + return fromProb(parseFloat(event.target.value) / 100) +} From 538ae323d7d6b5585bda4efe76db7b3f8a5caa1c Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Tue, 7 Jun 2022 08:54:17 -0700 Subject: [PATCH 02/14] Floor tick values --- common/calculate-swap3.ts | 2 +- web/pages/swap.tsx | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/common/calculate-swap3.ts b/common/calculate-swap3.ts index fc552e2b..5819ae8e 100644 --- a/common/calculate-swap3.ts +++ b/common/calculate-swap3.ts @@ -91,5 +91,5 @@ function toProb(tick: number) { export function fromProb(prob: number) { const ratio = prob / (1 - prob) - return Math.log(ratio) / Math.log(1.0001) + return Math.floor(Math.log(ratio) / Math.log(1.0001)) } diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx index 9a22b11d..f5bbe7de 100644 --- a/web/pages/swap.tsx +++ b/web/pages/swap.tsx @@ -86,16 +86,12 @@ function PoolTable(props: { pool: Swap3Pool }) { ) } -// Stored as optimization. equal to floor(log_sqrt_1.0001(sqrtRatio)) -function tick(sqrtRatio: number) { - return Math.floor(Math.log(sqrtRatio) / Math.log(1.0001)) -} - export default function Swap() { const [pool, setPool] = useState({ liquidity: 100, sqrtRatio: 2, - tick: tick(0.1), + tick: fromProb(0.3), + ticks: [], }) const [minTick, setMinTick] = useState(0) From ab3b88112fc962e4f87b8a94dc9043145ec649eb Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Tue, 7 Jun 2022 09:36:42 -0700 Subject: [PATCH 03/14] Graph the amount of liquidity available --- web/components/contract/liquidity-graph.tsx | 101 ++++++++++++++++++++ web/pages/swap.tsx | 18 +++- 2 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 web/components/contract/liquidity-graph.tsx diff --git a/web/components/contract/liquidity-graph.tsx b/web/components/contract/liquidity-graph.tsx new file mode 100644 index 00000000..d85585ab --- /dev/null +++ b/web/components/contract/liquidity-graph.tsx @@ -0,0 +1,101 @@ +import { DatumValue } from '@nivo/core' +import { Point, ResponsiveLine } from '@nivo/line' +import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants' +import { memo } from 'react' +import { range } from 'lodash' +import { getDpmOutcomeProbabilities } from '../../../common/calculate-dpm' +import { NumericContract } from '../../../common/contract' +import { useWindowSize } from '../../hooks/use-window-size' +import { Col } from '../layout/col' +import { formatLargeNumber } from 'common/util/format' + +export type GraphPoint = { + // A probability between 0 and 1 + x: number + // Amount of liquidity + y: number +} + +export const LiquidityGraph = memo(function NumericGraph(props: { + min?: 0 + max?: 1 + points: GraphPoint[] + height?: number +}) { + const { height, min, max, points } = props + + // Really maxLiquidity + const maxLiquidity = 500 + const data = [{ id: 'Probability', data: points, color: NUMERIC_GRAPH_COLOR }] + + const yTickValues = [ + 0, + 0.25 * maxLiquidity, + 0.5 * maxLiquidity, + 0.75 * maxLiquidity, + maxLiquidity, + ] + + const { width } = useWindowSize() + + const numXTickValues = !width || width < 800 ? 2 : 5 + + return ( +
= 800 ? 350 : 250) }} + > + `${formatLargeNumber(+d, 3)}`} + axisBottom={{ + tickValues: numXTickValues, + format: (d) => `${formatLargeNumber(+d, 3)}`, + }} + colors={{ datum: 'color' }} + pointSize={0} + enableSlices="x" + sliceTooltip={({ slice }) => { + const point = slice.points[0] + return + }} + enableGridX={!!width && width >= 800} + enableArea + margin={{ top: 20, right: 28, bottom: 22, left: 50 }} + /> +
+ ) +}) + +function formatLiquidity(y: DatumValue) { + const p = Math.round(+y * 100) / 100 + return `${p}L` +} + +function Tooltip(props: { point: Point }) { + const { point } = props + return ( + +
+ {point.serieId} {point.data.yFormatted} +
+
{formatLargeNumber(+point.data.x)}
+ + ) +} diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx index f5bbe7de..b213b8ae 100644 --- a/web/pages/swap.tsx +++ b/web/pages/swap.tsx @@ -6,6 +6,7 @@ import { } from 'common/calculate-swap3' import { formatPercent } from 'common/util/format' import { useState } from 'react' +import { LiquidityGraph } from 'web/components/contract/liquidity-graph' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' @@ -86,12 +87,24 @@ function PoolTable(props: { pool: Swap3Pool }) { ) } +function Graph(props: { pool: Swap3Pool }) { + const points = [ + { x: 0, y: 100 }, + { x: 0.2, y: 100 }, + { x: 0.2, y: 200 }, + { x: 0.33, y: 200 }, + { x: 0.33, y: 100 }, + { x: 1, y: 100 }, + ] + return +} + export default function Swap() { const [pool, setPool] = useState({ liquidity: 100, sqrtRatio: 2, tick: fromProb(0.3), - ticks: [], + tickStates: [], }) const [minTick, setMinTick] = useState(0) @@ -106,8 +119,9 @@ export default function Swap() { return ( - + {/* */} + Date: Tue, 7 Jun 2022 09:46:36 -0700 Subject: [PATCH 04/14] Remove sqrtRatio for now; track tickStates --- common/calculate-swap3.ts | 69 +++++++++++++++++++++++++++++++++++++-- web/pages/swap.tsx | 19 +++++------ 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/common/calculate-swap3.ts b/common/calculate-swap3.ts index 5819ae8e..1b902647 100644 --- a/common/calculate-swap3.ts +++ b/common/calculate-swap3.ts @@ -1,4 +1,4 @@ -type Swap3LiquidityProvision = { +type Swap3LiquidityPosition = { // TODO: Record who added this stuff? // Not sure if this is needed; maybe YES and NO left @@ -26,6 +26,17 @@ type Swap3LiquidityProvision = { maxTick: number } +type TickState = { + tick: number + + // Amount of liquidity added when crossing this tick from left to right + // Negative if we should remove liquidity + liquidityNet: number + + // Total liquidity referencing this pool + liquidityGross: number +} + // From https://uniswap.org/whitepaper-v3.pdf export type Swap3Pool = { // id: string @@ -35,12 +46,24 @@ export type Swap3Pool = { // 6.2 Global State liquidity: number // = sqrt(NY) - sqrtRatio: number // = sqrt(N / Y); N = # NO shares in pool + // sqrtRatio: number // = sqrt(N / Y); N = # NO shares in pool // So N = liquidity * sqrtRatio; Y = liquidity / sqrtRatio - tick: number + // Current tick number. // Stored as optimization. equal to floor(log_sqrt_1.0001(sqrtRatio)) + tick: number // TODO add fees? + + // Mapping of tick indices to tick values. + tickStates: TickState[] +} + +export function noShares(pool: Swap3Pool) { + return pool.liquidity * toRatio(pool.tick) ** 0.5 +} + +export function yesShares(pool: Swap3Pool) { + return pool.liquidity / toRatio(pool.tick) ** 0.5 } export function getSwap3Probability(pool: Swap3Pool) { @@ -68,6 +91,7 @@ export function calculateLPCost( maxTick: number, deltaL: number ) { + // TODO: this is subtly wrong, because of rounding between curTick and sqrtPrice const upperTick = Math.min(maxTick, Math.max(minTick, curTick)) const costN = toRatio(upperTick) ** 0.5 - toRatio(minTick) ** 0.5 @@ -80,6 +104,44 @@ export function calculateLPCost( } } +// TODO: Untested +function addPosition( + pool: Swap3Pool, + minTick: number, + maxTick: number, + deltaL: number +) { + const { requiredN, requiredY } = calculateLPCost( + pool.tick, + minTick, + maxTick, + deltaL + ) + console.log(`Deducting required N: ${requiredN} and required Y: ${requiredY}`) + + // Add liquidity as we pass through the larger tick + const maxTickState = pool.tickStates[maxTick] || { + tick: maxTick, + liquidityNet: 0, + liquidityGross: 0, + } + + maxTickState.liquidityNet += deltaL + maxTickState.liquidityGross += deltaL + + // And remove it as we pass through the lower one + const minTickState = pool.tickStates[minTick] || { + tick: minTick, + liquidityNet: 0, + liquidityGross: 0, + } + + minTickState.liquidityNet -= deltaL + minTickState.liquidityGross -= deltaL + + // TODO: add deltaL to liquidityGross of tickStates between minTick and maxTick +} + function toRatio(tick: number) { return 1.0001 ** tick } @@ -89,6 +151,7 @@ function toProb(tick: number) { return ratio / (ratio + 1) } +// Returns the tick for a given probability from 0 to 1 export function fromProb(prob: number) { const ratio = prob / (1 - prob) return Math.floor(Math.log(ratio) / Math.log(1.0001)) diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx index b213b8ae..32de6055 100644 --- a/web/pages/swap.tsx +++ b/web/pages/swap.tsx @@ -2,7 +2,9 @@ import { calculateLPCost, fromProb, getSwap3Probability, + noShares, Swap3Pool, + yesShares, } from 'common/calculate-swap3' import { formatPercent } from 'common/util/format' import { useState } from 'react' @@ -63,24 +65,20 @@ function PoolTable(props: { pool: Swap3Pool }) { {pool.liquidity} -
- - {pool.sqrtRatio} -
{pool.tick}
- {pool.liquidity * pool.sqrtRatio} + {yesShares(pool).toFixed(2)}
- {pool.liquidity / pool.sqrtRatio} + {noShares(pool).toFixed(2)}
- + {formatPercent(getSwap3Probability(pool))}
@@ -102,7 +100,6 @@ function Graph(props: { pool: Swap3Pool }) { export default function Swap() { const [pool, setPool] = useState({ liquidity: 100, - sqrtRatio: 2, tick: fromProb(0.3), tickStates: [], }) @@ -124,7 +121,7 @@ export default function Swap() { setPool((p) => ({ @@ -139,14 +136,14 @@ export default function Swap() { setMinTick(inputPercentToTick(e))} /> Min Tick: {minTick} setMaxTick(inputPercentToTick(e))} /> From 81064e67cfb76cac9fa8e38dcbef90a25eb2bd61 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 8 Jun 2022 08:45:09 -0700 Subject: [PATCH 05/14] Show tickStates in graph and table --- common/calculate-swap3.ts | 39 ++++++++------ web/pages/swap.tsx | 110 ++++++++++++++++++++++++-------------- 2 files changed, 95 insertions(+), 54 deletions(-) diff --git a/common/calculate-swap3.ts b/common/calculate-swap3.ts index 1b902647..ce66ea6a 100644 --- a/common/calculate-swap3.ts +++ b/common/calculate-swap3.ts @@ -105,7 +105,8 @@ export function calculateLPCost( } // TODO: Untested -function addPosition( +// Currently, this mutates the pool. Should it return a new object instead? +export function addPosition( pool: Swap3Pool, minTick: number, maxTick: number, @@ -119,34 +120,42 @@ function addPosition( ) console.log(`Deducting required N: ${requiredN} and required Y: ${requiredY}`) - // Add liquidity as we pass through the larger tick - const maxTickState = pool.tickStates[maxTick] || { - tick: maxTick, - liquidityNet: 0, - liquidityGross: 0, - } - - maxTickState.liquidityNet += deltaL - maxTickState.liquidityGross += deltaL - - // And remove it as we pass through the lower one + // Add liquidity as we pass through the smaller tick const minTickState = pool.tickStates[minTick] || { tick: minTick, liquidityNet: 0, liquidityGross: 0, } - minTickState.liquidityNet -= deltaL - minTickState.liquidityGross -= deltaL + minTickState.liquidityNet += deltaL + minTickState.liquidityGross += deltaL + pool.tickStates[minTick] = minTickState + + // And remove it as we pass through the larger one + const maxTickState = pool.tickStates[maxTick] || { + tick: maxTick, + liquidityNet: 0, + liquidityGross: 0, + } + + maxTickState.liquidityNet -= deltaL + maxTickState.liquidityGross -= deltaL + pool.tickStates[maxTick] = maxTickState // TODO: add deltaL to liquidityGross of tickStates between minTick and maxTick + + return pool +} + +export function sortedTickStates(pool: Swap3Pool) { + return Object.values(pool.tickStates).sort((a, b) => a.tick - b.tick) } function toRatio(tick: number) { return 1.0001 ** tick } -function toProb(tick: number) { +export function toProb(tick: number) { const ratio = toRatio(tick) return ratio / (ratio + 1) } diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx index 32de6055..a188122c 100644 --- a/web/pages/swap.tsx +++ b/web/pages/swap.tsx @@ -1,9 +1,12 @@ import { + addPosition, calculateLPCost, fromProb, getSwap3Probability, noShares, + sortedTickStates, Swap3Pool, + toProb, yesShares, } from 'common/calculate-swap3' import { formatPercent } from 'common/util/format' @@ -60,40 +63,63 @@ function BalanceTable() { function PoolTable(props: { pool: Swap3Pool }) { const { pool } = props return ( - -
- - {pool.liquidity} -
-
- - {pool.tick} -
-
- - {yesShares(pool).toFixed(2)} -
-
- - {noShares(pool).toFixed(2)} -
-
- - {formatPercent(getSwap3Probability(pool))} -
-
+ <> + +
+ + {formatPercent(getSwap3Probability(pool))} +
+ +
+ + {pool.liquidity} +
+
+ + {pool.tick} +
+
+ + {yesShares(pool).toFixed(2)} +
+
+ + {noShares(pool).toFixed(2)} +
+
+ {/* Render each tickState as another row in a table */} + + + + + + + + + {sortedTickStates(pool).map((tickState, i) => ( + + + + + + ))} +
TickProbNet Liquidity
{tickState.tick} + {formatPercent(toProb(tickState.tick))} + {tickState.liquidityNet}
+ ) } function Graph(props: { pool: Swap3Pool }) { - const points = [ - { x: 0, y: 100 }, - { x: 0.2, y: 100 }, - { x: 0.2, y: 200 }, - { x: 0.33, y: 200 }, - { x: 0.33, y: 100 }, - { x: 1, y: 100 }, - ] + const { pool } = props + let liquidity = 100 // TODO unhardcode + const points = [{ x: 0, y: liquidity }] + for (const tickState of sortedTickStates(pool)) { + points.push({ x: toProb(tickState.tick), y: liquidity }) + liquidity += tickState.liquidityNet + points.push({ x: toProb(tickState.tick), y: liquidity }) + } + points.push({ x: 1, y: liquidity }) return } @@ -115,7 +141,7 @@ export default function Swap() { ) return ( - + {/* */} @@ -123,17 +149,15 @@ export default function Swap() { className="input" placeholder="Current%" type="number" - onChange={(e) => - setPool((p) => ({ - ...p, - tick: inputPercentToTick(e), - })) - } + onChange={(e) => { + pool.tick = inputPercentToTick(e) + setPool({ ...pool }) + }} /> Alice: Add liquidity - + {/* */} Y required: {requiredY}
N required: {requiredN}
{' '} - + From 2fdb962a14bfb60987bd201dbc4d397cca692d3c Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 8 Jun 2022 08:57:15 -0700 Subject: [PATCH 06/14] Show current prob on graph --- web/components/contract/liquidity-graph.tsx | 17 +++++++++++++++-- web/pages/swap.tsx | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/web/components/contract/liquidity-graph.tsx b/web/components/contract/liquidity-graph.tsx index d85585ab..0c69bf8b 100644 --- a/web/components/contract/liquidity-graph.tsx +++ b/web/components/contract/liquidity-graph.tsx @@ -7,7 +7,7 @@ import { getDpmOutcomeProbabilities } from '../../../common/calculate-dpm' import { NumericContract } from '../../../common/contract' import { useWindowSize } from '../../hooks/use-window-size' import { Col } from '../layout/col' -import { formatLargeNumber } from 'common/util/format' +import { formatLargeNumber, formatPercent } from 'common/util/format' export type GraphPoint = { // A probability between 0 and 1 @@ -21,8 +21,9 @@ export const LiquidityGraph = memo(function NumericGraph(props: { max?: 1 points: GraphPoint[] height?: number + marker?: number // Value between min and max to highlight on x-axis }) { - const { height, min, max, points } = props + const { height, min, max, points, marker } = props // Really maxLiquidity const maxLiquidity = 500 @@ -73,6 +74,18 @@ export const LiquidityGraph = memo(function NumericGraph(props: { enableGridX={!!width && width >= 800} enableArea margin={{ top: 20, right: 28, bottom: 22, left: 50 }} + markers={ + marker + ? [ + { + axis: 'x', + value: marker, + lineStyle: { stroke: '#000', strokeWidth: 2 }, + legend: `Implied: ${formatPercent(marker)}`, + }, + ] + : [] + } /> ) diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx index a188122c..f2207cf3 100644 --- a/web/pages/swap.tsx +++ b/web/pages/swap.tsx @@ -120,7 +120,7 @@ function Graph(props: { pool: Swap3Pool }) { points.push({ x: toProb(tickState.tick), y: liquidity }) } points.push({ x: 1, y: liquidity }) - return + return } export default function Swap() { From 853c45d5642585ba9ec684a3448615d065b13a48 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 8 Jun 2022 09:10:34 -0700 Subject: [PATCH 07/14] Make typescript happy --- web/pages/swap.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx index f2207cf3..376a1601 100644 --- a/web/pages/swap.tsx +++ b/web/pages/swap.tsx @@ -15,7 +15,7 @@ import { LiquidityGraph } from 'web/components/contract/liquidity-graph' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' -const users = { +const users: Record = { alice: { M: 100, YES: 0, From 17ae9d953d6dae51c0ea9af6431b842a9504db0e Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 8 Jun 2022 10:36:35 -0700 Subject: [PATCH 08/14] Calculate gross liquidity --- common/calculate-swap3.ts | 20 +++++++++++--------- web/pages/swap.tsx | 28 +++++++++++++++++++--------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/common/calculate-swap3.ts b/common/calculate-swap3.ts index ce66ea6a..a2f957a8 100644 --- a/common/calculate-swap3.ts +++ b/common/calculate-swap3.ts @@ -78,12 +78,9 @@ export function getSwap3Probability(pool: Swap3Pool) { function calculatePurchase( pool: Swap3Pool, - amount: number, + amount: number, // In M$ outcome: 'YES' | 'NO' -) { - const shares = 10 - const newPool = {} -} +) {} export function calculateLPCost( curTick: number, @@ -104,7 +101,6 @@ export function calculateLPCost( } } -// TODO: Untested // Currently, this mutates the pool. Should it return a new object instead? export function addPosition( pool: Swap3Pool, @@ -128,7 +124,6 @@ export function addPosition( } minTickState.liquidityNet += deltaL - minTickState.liquidityGross += deltaL pool.tickStates[minTick] = minTickState // And remove it as we pass through the larger one @@ -139,11 +134,18 @@ export function addPosition( } maxTickState.liquidityNet -= deltaL - maxTickState.liquidityGross -= deltaL pool.tickStates[maxTick] = maxTickState - // TODO: add deltaL to liquidityGross of tickStates between minTick and maxTick + return pool +} +// This also mutates the pool directly +export function grossLiquidity(pool: Swap3Pool) { + let liquidityGross = 0 + for (const tickState of sortedTickStates(pool)) { + liquidityGross += tickState.liquidityNet + tickState.liquidityGross = liquidityGross + } return pool } diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx index 376a1601..edeb6c48 100644 --- a/web/pages/swap.tsx +++ b/web/pages/swap.tsx @@ -3,6 +3,7 @@ import { calculateLPCost, fromProb, getSwap3Probability, + grossLiquidity, noShares, sortedTickStates, Swap3Pool, @@ -14,6 +15,7 @@ import { useState } from 'react' import { LiquidityGraph } from 'web/components/contract/liquidity-graph' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' +import { addLiquidity } from 'web/lib/firebase/fn-call' const users: Record = { alice: { @@ -94,6 +96,7 @@ function PoolTable(props: { pool: Swap3Pool }) { Tick Prob Net Liquidity + Gross Liquidity {sortedTickStates(pool).map((tickState, i) => ( @@ -103,6 +106,7 @@ function PoolTable(props: { pool: Swap3Pool }) { {formatPercent(toProb(tickState.tick))} {tickState.liquidityNet} + {tickState.liquidityGross} ))} @@ -112,23 +116,28 @@ function PoolTable(props: { pool: Swap3Pool }) { function Graph(props: { pool: Swap3Pool }) { const { pool } = props - let liquidity = 100 // TODO unhardcode - const points = [{ x: 0, y: liquidity }] + const points = [] + let lastGross = 0 for (const tickState of sortedTickStates(pool)) { - points.push({ x: toProb(tickState.tick), y: liquidity }) - liquidity += tickState.liquidityNet - points.push({ x: toProb(tickState.tick), y: liquidity }) + const { tick, liquidityGross } = tickState + points.push({ x: toProb(tick), y: lastGross }) + points.push({ x: toProb(tick), y: liquidityGross }) + lastGross = liquidityGross } - points.push({ x: 1, y: liquidity }) return } export default function Swap() { - const [pool, setPool] = useState({ - liquidity: 100, + // Set up an initial pool with 100 liquidity from 0% to 100% + // TODO: Not sure why maxTick of 2**23 breaks it, but 2**20 is okay... + let INIT_POOL: Swap3Pool = { + liquidity: 0, tick: fromProb(0.3), tickStates: [], - }) + } + INIT_POOL = addPosition(INIT_POOL, -(2 ** 23), 2 ** 20, 100) + INIT_POOL = grossLiquidity(INIT_POOL) + const [pool, setPool] = useState(INIT_POOL) const [minTick, setMinTick] = useState(0) const [maxTick, setMaxTick] = useState(0) @@ -180,6 +189,7 @@ export default function Swap() { className="btn" onClick={() => { addPosition(pool, minTick, maxTick, 100) + grossLiquidity(pool) setPool({ ...pool }) }} > From 06c49be05bf6944417569d9620cb639290ed26b8 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 8 Jun 2022 19:56:59 -0700 Subject: [PATCH 09/14] Implement buy YES calculation --- common/calculate-swap3.ts | 61 +++++++++++++++++++++++++++++++++++++++ web/pages/swap.tsx | 20 +++++++++++-- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/common/calculate-swap3.ts b/common/calculate-swap3.ts index a2f957a8..a40fe54a 100644 --- a/common/calculate-swap3.ts +++ b/common/calculate-swap3.ts @@ -89,6 +89,7 @@ export function calculateLPCost( deltaL: number ) { // TODO: this is subtly wrong, because of rounding between curTick and sqrtPrice + // Also below in buyYES const upperTick = Math.min(maxTick, Math.max(minTick, curTick)) const costN = toRatio(upperTick) ** 0.5 - toRatio(minTick) ** 0.5 @@ -101,6 +102,62 @@ export function calculateLPCost( } } +// Returns a preview of the new pool + number of YES shares purchased. +// Does NOT modify the pool +// Hm, logic is pretty complicated. Let's see if we can simplify this. +export function buyYes( + pool: Swap3Pool, + amount: number // In M$ +) { + const tickStates = sortedTickStates(pool) + let tick = pool.tick + let stateIndex = 0 + let amountLeft = amount + let yesPurchased = 0 + while (amountLeft > 0) { + // Find the current tick state + while (tick >= tickStates[stateIndex + 1].tick) { + stateIndex++ + if (stateIndex > tickStates.length - 2) { + // We've reached the end of the tick states... + throw new Error('Ran out of tick states') + } + } + const state = tickStates[stateIndex] + const nextState = tickStates[stateIndex + 1] + + // Copied from above; TODO extract to common function + const noCost = toRatio(nextState.tick) ** 0.5 - toRatio(tick) ** 0.5 + const yesCost = + 1 / toRatio(tick) ** 0.5 - 1 / toRatio(nextState.tick) ** 0.5 + + if (noCost * state.liquidityGross <= amountLeft) { + // We can fully purchase up until the next tick state + amountLeft -= noCost * state.liquidityGross + yesPurchased += yesCost * state.liquidityGross + tick = nextState.tick + } else { + // Buy as much as we can at the current tick state. Derivation: + // noCostLeft = toRatio(upTick) ** 0.5 - toRatio(tick) ** 0.5 + // (noCostLeft + toRatio(tick) ** 0.5) ** 2 = toRatio(upTick) + // TODO check flooring done here + const noCostLeft = amountLeft / state.liquidityGross + const finalTick = fromRatio((noCostLeft + toRatio(tick) ** 0.5) ** 2) + const yesCostLeft = + 1 / toRatio(tick) ** 0.5 - 1 / toRatio(finalTick) ** 0.5 + + amountLeft = 0 + yesPurchased += yesCostLeft * state.liquidityGross + tick = finalTick + } + } + + return { + newPoolTick: tick, + yesPurchased, + } +} + // Currently, this mutates the pool. Should it return a new object instead? export function addPosition( pool: Swap3Pool, @@ -165,5 +222,9 @@ export function toProb(tick: number) { // Returns the tick for a given probability from 0 to 1 export function fromProb(prob: number) { const ratio = prob / (1 - prob) + return fromRatio(ratio) +} + +function fromRatio(ratio: number) { return Math.floor(Math.log(ratio) / Math.log(1.0001)) } diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx index edeb6c48..6e5aeb7b 100644 --- a/web/pages/swap.tsx +++ b/web/pages/swap.tsx @@ -1,5 +1,6 @@ import { addPosition, + buyYes, calculateLPCost, fromProb, getSwap3Probability, @@ -136,11 +137,14 @@ export default function Swap() { tickStates: [], } INIT_POOL = addPosition(INIT_POOL, -(2 ** 23), 2 ** 20, 100) + INIT_POOL = addPosition(INIT_POOL, fromProb(0.32), fromProb(0.35), 100) INIT_POOL = grossLiquidity(INIT_POOL) + const [pool, setPool] = useState(INIT_POOL) const [minTick, setMinTick] = useState(0) const [maxTick, setMaxTick] = useState(0) + const [buyAmount, setBuyAmount] = useState(0) const { requiredN, requiredY } = calculateLPCost( pool.tick, @@ -149,6 +153,8 @@ export default function Swap() { 100 // deltaL ) + const { newPoolTick, yesPurchased } = buyYes(pool, buyAmount) + return ( {/* */} @@ -200,10 +206,20 @@ export default function Swap() { Bob: Buy Tokens {/* */} - + setBuyAmount(parseFloat(e.target.value))} + /> + +
Y shares purchaseable: {yesPurchased.toFixed(2)}
+
New Tick: {newPoolTick}
+
New prob: {formatPercent(toProb(newPoolTick))}
+
- + {/* */} From 7acd3aed930dc7453f7a4ad5e4b88f77a3076c02 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 8 Jun 2022 20:19:26 -0700 Subject: [PATCH 10/14] Simplify buying logic --- common/calculate-swap3.ts | 43 +++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/common/calculate-swap3.ts b/common/calculate-swap3.ts index a40fe54a..f67b290d 100644 --- a/common/calculate-swap3.ts +++ b/common/calculate-swap3.ts @@ -114,8 +114,9 @@ export function buyYes( let stateIndex = 0 let amountLeft = amount let yesPurchased = 0 - while (amountLeft > 0) { - // Find the current tick state + // Stop if there's epsilon M$ left, due to rounding issues + while (amountLeft > 1e-6) { + // Find the current & next states for this tick while (tick >= tickStates[stateIndex + 1].tick) { stateIndex++ if (stateIndex > tickStates.length - 2) { @@ -126,32 +127,22 @@ export function buyYes( const state = tickStates[stateIndex] const nextState = tickStates[stateIndex + 1] - // Copied from above; TODO extract to common function - const noCost = toRatio(nextState.tick) ** 0.5 - toRatio(tick) ** 0.5 - const yesCost = - 1 / toRatio(tick) ** 0.5 - 1 / toRatio(nextState.tick) ** 0.5 + // nextState.tick purchases through the bucket; fullTick uses the remaining amountLeft + const fullCostN = amountLeft / state.liquidityGross + // Note: fullTick is NOT floored here; it's for the sqrtPrice to buy up to + const fullTick = fromRatioUnfloored((fullCostN + toRatio(tick) ** 0.5) ** 2) + const nextTick = Math.min(nextState.tick, fullTick) - if (noCost * state.liquidityGross <= amountLeft) { - // We can fully purchase up until the next tick state - amountLeft -= noCost * state.liquidityGross - yesPurchased += yesCost * state.liquidityGross - tick = nextState.tick - } else { - // Buy as much as we can at the current tick state. Derivation: - // noCostLeft = toRatio(upTick) ** 0.5 - toRatio(tick) ** 0.5 - // (noCostLeft + toRatio(tick) ** 0.5) ** 2 = toRatio(upTick) - // TODO check flooring done here - const noCostLeft = amountLeft / state.liquidityGross - const finalTick = fromRatio((noCostLeft + toRatio(tick) ** 0.5) ** 2) - const yesCostLeft = - 1 / toRatio(tick) ** 0.5 - 1 / toRatio(finalTick) ** 0.5 + // Copied from above; TODO extract to common function? + const noCost = toRatio(nextTick) ** 0.5 - toRatio(tick) ** 0.5 + const yesCost = 1 / toRatio(tick) ** 0.5 - 1 / toRatio(nextTick) ** 0.5 - amountLeft = 0 - yesPurchased += yesCostLeft * state.liquidityGross - tick = finalTick - } + amountLeft -= noCost * state.liquidityGross + yesPurchased += yesCost * state.liquidityGross + tick = Math.floor(nextTick) } + // Right now we eat the epsilon amounntLeft as a fee. Could return it, shrug. return { newPoolTick: tick, yesPurchased, @@ -228,3 +219,7 @@ export function fromProb(prob: number) { function fromRatio(ratio: number) { return Math.floor(Math.log(ratio) / Math.log(1.0001)) } + +function fromRatioUnfloored(ratio: number) { + return Math.log(ratio) / Math.log(1.0001) +} From 4cd35c87427b3a26b85f5092587c0a22c2dc83b2 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 8 Jun 2022 20:29:23 -0700 Subject: [PATCH 11/14] Show a preview of where the buy goes to --- web/components/contract/liquidity-graph.tsx | 35 ++++++++++++--------- web/pages/swap.tsx | 19 ++++++++--- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/web/components/contract/liquidity-graph.tsx b/web/components/contract/liquidity-graph.tsx index 0c69bf8b..38f422ad 100644 --- a/web/components/contract/liquidity-graph.tsx +++ b/web/components/contract/liquidity-graph.tsx @@ -1,4 +1,4 @@ -import { DatumValue } from '@nivo/core' +import { CartesianMarkerProps, DatumValue } from '@nivo/core' import { Point, ResponsiveLine } from '@nivo/line' import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants' import { memo } from 'react' @@ -22,8 +22,9 @@ export const LiquidityGraph = memo(function NumericGraph(props: { points: GraphPoint[] height?: number marker?: number // Value between min and max to highlight on x-axis + previewMarker?: number }) { - const { height, min, max, points, marker } = props + const { height, min, max, points, marker, previewMarker } = props // Really maxLiquidity const maxLiquidity = 500 @@ -41,6 +42,23 @@ export const LiquidityGraph = memo(function NumericGraph(props: { const numXTickValues = !width || width < 800 ? 2 : 5 + const markers: CartesianMarkerProps[] = [] + if (marker) { + markers.push({ + axis: 'x', + value: marker, + lineStyle: { stroke: '#000', strokeWidth: 2 }, + legend: `Implied: ${formatPercent(marker)}`, + }) + } + if (previewMarker) { + markers.push({ + axis: 'x', + value: previewMarker, + lineStyle: { stroke: '#8888', strokeWidth: 2 }, + }) + } + return (
= 800} enableArea margin={{ top: 20, right: 28, bottom: 22, left: 50 }} - markers={ - marker - ? [ - { - axis: 'x', - value: marker, - lineStyle: { stroke: '#000', strokeWidth: 2 }, - legend: `Implied: ${formatPercent(marker)}`, - }, - ] - : [] - } + markers={markers} />
) diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx index 6e5aeb7b..7d99e546 100644 --- a/web/pages/swap.tsx +++ b/web/pages/swap.tsx @@ -115,8 +115,8 @@ function PoolTable(props: { pool: Swap3Pool }) { ) } -function Graph(props: { pool: Swap3Pool }) { - const { pool } = props +function Graph(props: { pool: Swap3Pool; previewMarker?: number }) { + const { pool, previewMarker } = props const points = [] let lastGross = 0 for (const tickState of sortedTickStates(pool)) { @@ -125,7 +125,13 @@ function Graph(props: { pool: Swap3Pool }) { points.push({ x: toProb(tick), y: liquidityGross }) lastGross = liquidityGross } - return + return ( + + ) } export default function Swap() { @@ -159,7 +165,12 @@ export default function Swap() { {/* */} - + Date: Fri, 10 Jun 2022 09:12:44 -0700 Subject: [PATCH 12/14] Tweak interface --- web/pages/swap.tsx | 53 +++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx index 7d99e546..3bc54a49 100644 --- a/web/pages/swap.tsx +++ b/web/pages/swap.tsx @@ -150,13 +150,14 @@ export default function Swap() { const [minTick, setMinTick] = useState(0) const [maxTick, setMaxTick] = useState(0) + const [deltaL, setDeltaL] = useState(100) const [buyAmount, setBuyAmount] = useState(0) const { requiredN, requiredY } = calculateLPCost( pool.tick, minTick, maxTick, - 100 // deltaL + deltaL ) const { newPoolTick, yesPurchased } = buyYes(pool, buyAmount) @@ -182,25 +183,34 @@ export default function Swap() { /> - Alice: Add liquidity +

Add liquidity

{/* */} - setMinTick(inputPercentToTick(e))} - /> - Min Tick: {minTick} - setMaxTick(inputPercentToTick(e))} - /> - Max Tick: {maxTick} + + setMinTick(inputPercentToTick(e))} + /> + {/* Min Tick: {minTick} */} + setMaxTick(inputPercentToTick(e))} + /> + {/* Max Tick: {maxTick} */} + setDeltaL(parseFloat(e.target.value))} + /> + -
Y required: {requiredY}
-
N required: {requiredN}
{' '} +
Y required: {requiredY.toFixed(2)}
+
N required: {requiredN.toFixed(2)}
{' '}
+ + ) +} + export default function Swap() { // Set up an initial pool with 100 liquidity from 0% to 100% // TODO: Not sure why maxTick of 2**23 breaks it, but 2**20 is okay... @@ -147,19 +208,8 @@ export default function Swap() { INIT_POOL = grossLiquidity(INIT_POOL) const [pool, setPool] = useState(INIT_POOL) - - const [minTick, setMinTick] = useState(0) - const [maxTick, setMaxTick] = useState(0) - const [deltaL, setDeltaL] = useState(100) const [buyAmount, setBuyAmount] = useState(0) - const { requiredN, requiredY } = calculateLPCost( - pool.tick, - minTick, - maxTick, - deltaL - ) - const { newPoolTick, yesPurchased } = buyYes(pool, buyAmount) return ( @@ -182,47 +232,7 @@ export default function Swap() { }} /> - -

Add liquidity

- {/* */} - - setMinTick(inputPercentToTick(e))} - /> - {/* Min Tick: {minTick} */} - setMaxTick(inputPercentToTick(e))} - /> - {/* Max Tick: {maxTick} */} - setDeltaL(parseFloat(e.target.value))} - /> - - -
Y required: {requiredY.toFixed(2)}
-
N required: {requiredN.toFixed(2)}
{' '} -
- - +

Limit Order

From 989fa1306bcbb39f58ad486c1304454c2827f955 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Sat, 11 Jun 2022 09:15:07 -0700 Subject: [PATCH 14/14] Implement Balancer math on Uv3 --- common/calculate-swap3.ts | 37 ++++++++++++++++++++++++++++++++++++- web/pages/swap.tsx | 8 +++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/common/calculate-swap3.ts b/common/calculate-swap3.ts index f67b290d..4e648d75 100644 --- a/common/calculate-swap3.ts +++ b/common/calculate-swap3.ts @@ -162,7 +162,7 @@ export function addPosition( maxTick, deltaL ) - console.log(`Deducting required N: ${requiredN} and required Y: ${requiredY}`) + // console.log(`Deducting required N: ${requiredN} and required Y: ${requiredY}`) // Add liquidity as we pass through the smaller tick const minTickState = pool.tickStates[minTick] || { @@ -187,6 +187,41 @@ export function addPosition( return pool } +export function addBalancer(pool: Swap3Pool, p: number, deltaL: number) { + // TODO: math is borked, shouldn't be returning infinity + function tickL(tick: number) { + const q = 1 - p + return deltaL * 2 * p ** q * q ** p * 1.0001 ** ((p - 0.5) * tick) + } + + // See how much liquidity is provided at +/- 5pp around p + // const minTick = fromProb(p - 0.1) + // const maxTick = fromProb(p + 0.05) + const minTick = fromProb(0.000001) + const maxTick = fromProb(0.999999) + let totalN = 0 + let totalY = 0 + const stride = 300 + for (let t = minTick; t <= maxTick; t += stride) { + // console.log('liquidity at tick ', t, toProb(t), tickL(t)) + const { requiredN, requiredY } = calculateLPCost( + fromProb(p), + t, + t + stride, + tickL(t) + ) + totalN += requiredN + totalY += requiredY + // Add liquidity + addPosition(pool, t, t + stride, tickL(t)) + } + + console.log('rough number of ticks', (maxTick - minTick) / stride) + console.log(`Total N: ${totalN} and total Y: ${totalY}`) + grossLiquidity(pool) + return pool +} + // This also mutates the pool directly export function grossLiquidity(pool: Swap3Pool) { let liquidityGross = 0 diff --git a/web/pages/swap.tsx b/web/pages/swap.tsx index b347ab83..e0158f67 100644 --- a/web/pages/swap.tsx +++ b/web/pages/swap.tsx @@ -1,4 +1,5 @@ import { + addBalancer, addPosition, buyYes, calculateLPCost, @@ -203,8 +204,9 @@ export default function Swap() { tick: fromProb(0.3), tickStates: [], } - INIT_POOL = addPosition(INIT_POOL, -(2 ** 23), 2 ** 20, 100) - INIT_POOL = addPosition(INIT_POOL, fromProb(0.32), fromProb(0.35), 100) + INIT_POOL = addPosition(INIT_POOL, -(2 ** 23), 2 ** 20, 1) + // INIT_POOL = addPosition(INIT_POOL, fromProb(0.32), fromProb(0.35), 100) + INIT_POOL = addBalancer(INIT_POOL, 0.3, 100) INIT_POOL = grossLiquidity(INIT_POOL) const [pool, setPool] = useState(INIT_POOL) @@ -215,7 +217,7 @@ export default function Swap() { return ( {/* */} - + {/* */}