Compare commits

...

15 Commits
main ... swap3

Author SHA1 Message Date
Austin Chen
2cf672fac1 Merge branch 'main' into swap3 2022-06-25 16:48:00 -05:00
Austin Chen
989fa1306b Implement Balancer math on Uv3 2022-06-11 09:15:07 -07:00
Austin Chen
6ac29106f2 Extract out liquidity component 2022-06-10 09:22:40 -07:00
Austin Chen
3898ec95ed Tweak interface 2022-06-10 09:12:44 -07:00
Austin Chen
4cd35c8742 Show a preview of where the buy goes to 2022-06-08 20:29:23 -07:00
Austin Chen
7acd3aed93 Simplify buying logic 2022-06-08 20:19:26 -07:00
Austin Chen
06c49be05b Implement buy YES calculation 2022-06-08 19:56:59 -07:00
Austin Chen
17ae9d953d Calculate gross liquidity 2022-06-08 10:36:35 -07:00
Austin Chen
853c45d564 Make typescript happy 2022-06-08 09:10:34 -07:00
Austin Chen
2fdb962a14 Show current prob on graph 2022-06-08 08:57:15 -07:00
Austin Chen
81064e67cf Show tickStates in graph and table 2022-06-08 08:45:09 -07:00
Austin Chen
8734a14e6b Remove sqrtRatio for now; track tickStates 2022-06-07 09:46:36 -07:00
Austin Chen
ab3b88112f Graph the amount of liquidity available 2022-06-07 09:36:42 -07:00
Austin Chen
538ae323d7 Floor tick values 2022-06-07 08:54:17 -07:00
Austin Chen
4b09640e60 Create a test interface for Uniswap v3 2022-06-07 08:37:00 -07:00
3 changed files with 650 additions and 0 deletions

260
common/calculate-swap3.ts Normal file
View File

@ -0,0 +1,260 @@
type Swap3LiquidityPosition = {
// 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
}
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
// 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
// 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) {
// 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, // In M$
outcome: 'YES' | 'NO'
) {}
export function calculateLPCost(
curTick: number,
minTick: number,
maxTick: number,
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
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,
}
}
// 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
// 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) {
// 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]
// 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)
// 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 -= 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,
}
}
// Currently, this mutates the pool. Should it return a new object instead?
export 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 smaller tick
const minTickState = pool.tickStates[minTick] || {
tick: minTick,
liquidityNet: 0,
liquidityGross: 0,
}
minTickState.liquidityNet += 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
pool.tickStates[maxTick] = maxTickState
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
for (const tickState of sortedTickStates(pool)) {
liquidityGross += tickState.liquidityNet
tickState.liquidityGross = liquidityGross
}
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
}
export function toProb(tick: number) {
const ratio = toRatio(tick)
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 fromRatio(ratio)
}
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)
}

View File

@ -0,0 +1,121 @@
import { CartesianMarkerProps, 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, formatPercent } 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
marker?: number // Value between min and max to highlight on x-axis
previewMarker?: number
}) {
const { height, min, max, points, marker, previewMarker } = 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
const markers: CartesianMarkerProps<DatumValue>[] = []
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 (
<div
className="w-full overflow-hidden"
style={{ height: height ?? (!width || width >= 800 ? 350 : 250) }}
>
<ResponsiveLine
data={data}
yScale={{ min: 0, max: maxLiquidity, type: 'linear' }}
yFormat={formatLiquidity}
axisLeft={{
tickValues: yTickValues,
format: formatLiquidity,
}}
xScale={{
type: 'linear',
min: min,
max: max,
}}
xFormat={(d) => `${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 <Tooltip point={point} />
}}
enableGridX={!!width && width >= 800}
enableArea
margin={{ top: 20, right: 28, bottom: 22, left: 50 }}
markers={markers}
/>
</div>
)
})
function formatLiquidity(y: DatumValue) {
const p = Math.round(+y * 100) / 100
return `${p}L`
}
function Tooltip(props: { point: Point }) {
const { point } = props
return (
<Col className="border border-gray-300 bg-white py-2 px-3">
<div
className="pb-1"
style={{
color: point.serieColor,
}}
>
<strong>{point.serieId}</strong> {point.data.yFormatted}
</div>
<div>{formatLargeNumber(+point.data.x)}</div>
</Col>
)
}

269
web/pages/swap.tsx Normal file
View File

@ -0,0 +1,269 @@
import {
addBalancer,
addPosition,
buyYes,
calculateLPCost,
fromProb,
getSwap3Probability,
grossLiquidity,
noShares,
sortedTickStates,
Swap3Pool,
toProb,
yesShares,
} 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'
import { addLiquidity } from 'web/lib/firebase/fn-call'
const users: Record<string, any> = {
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 (
<table className="w-full">
<thead>
<tr>
<th className="px-4 py-2">User</th>
<th className="px-4 py-2">M</th>
<th className="px-4 py-2">YES</th>
<th className="px-4 py-2">NO</th>
</tr>
</thead>
<tbody>
{Object.keys(users).map((user) => (
<tr key={user}>
<td className="px-4 py-2">{user}</td>
<td className="px-4 py-2">{users[user].M}</td>
<td className="px-4 py-2">{users[user].YES}</td>
<td className="px-4 py-2">{users[user].NO}</td>
</tr>
))}
</tbody>
</table>
)
}
/* Show the values in pool */
function PoolTable(props: { pool: Swap3Pool }) {
const { pool } = props
return (
<>
<Row className="gap-4">
<div>
<label>Implied: </label>
{formatPercent(getSwap3Probability(pool))}
</div>
<div>
<label>Liquidity: </label>
{pool.liquidity}
</div>
<div>
<label>Tick: </label>
{pool.tick}
</div>
<div>
<label>Pool YES: </label>
{yesShares(pool).toFixed(2)}
</div>
<div>
<label>Pool NO: </label>
{noShares(pool).toFixed(2)}
</div>
</Row>
{/* Render each tickState as another row in a table */}
<table className="w-full">
<thead>
<tr>
<th className="px-4 py-2">Tick</th>
<th className="px-4 py-2">Prob</th>
<th className="px-4 py-2">Net Liquidity</th>
<th className="px-4 py-2">Gross Liquidity</th>
</tr>
</thead>
{sortedTickStates(pool).map((tickState, i) => (
<tr key={i}>
<td className="px-4 py-2">{tickState.tick}</td>
<td className="px-4 py-2">
{formatPercent(toProb(tickState.tick))}
</td>
<td className="px-4 py-2">{tickState.liquidityNet}</td>
<td className="px-4 py-2">{tickState.liquidityGross}</td>
</tr>
))}
</table>
</>
)
}
function Graph(props: { pool: Swap3Pool; previewMarker?: number }) {
const { pool, previewMarker } = props
const points = []
let lastGross = 0
for (const tickState of sortedTickStates(pool)) {
const { tick, liquidityGross } = tickState
points.push({ x: toProb(tick), y: lastGross })
points.push({ x: toProb(tick), y: liquidityGross })
lastGross = liquidityGross
}
return (
<LiquidityGraph
points={points}
marker={toProb(pool.tick)}
previewMarker={previewMarker}
/>
)
}
function LiquidityPanel(props: {
pool: Swap3Pool
setPool: (pool: Swap3Pool) => void
}) {
const { pool, setPool } = props
const [minTick, setMinTick] = useState(0)
const [maxTick, setMaxTick] = useState(0)
const [deltaL, setDeltaL] = useState(100)
const { requiredN, requiredY } = calculateLPCost(
pool.tick,
minTick,
maxTick,
deltaL
)
return (
<Col>
<h2 className="my-2 text-xl">Add liquidity</h2>
{/* <input className="input" placeholder="Amount" type="number" /> */}
<Row className="gap-2">
<input
className="input"
placeholder="Min%"
type="number"
onChange={(e) => setMinTick(inputPercentToTick(e))}
/>
{/* Min Tick: {minTick} */}
<input
className="input"
placeholder="Max%"
type="number"
onChange={(e) => setMaxTick(inputPercentToTick(e))}
/>
{/* Max Tick: {maxTick} */}
<input
className="input"
placeholder="delta Liquidity"
type="number"
value={deltaL}
onChange={(e) => setDeltaL(parseFloat(e.target.value))}
/>
</Row>
<Row className="gap-2 py-2">
<div>Y required: {requiredY.toFixed(2)}</div>
<div>N required: {requiredN.toFixed(2)}</div>{' '}
</Row>
<button
className="btn"
onClick={() => {
addPosition(pool, minTick, maxTick, deltaL)
grossLiquidity(pool)
setPool({ ...pool })
}}
>
Create pool
</button>
</Col>
)
}
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...
let INIT_POOL: Swap3Pool = {
liquidity: 0,
tick: fromProb(0.3),
tickStates: [],
}
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)
const [buyAmount, setBuyAmount] = useState(0)
const { newPoolTick, yesPurchased } = buyYes(pool, buyAmount)
return (
<Col className="mx-auto max-w-2xl gap-10 p-4">
{/* <BalanceTable /> */}
{/* <PoolTable pool={pool} /> */}
<Graph
pool={pool}
previewMarker={
newPoolTick === pool.tick ? undefined : toProb(newPoolTick)
}
/>
<input
className="input"
placeholder="Current%"
type="number"
onChange={(e) => {
pool.tick = inputPercentToTick(e)
setPool({ ...pool })
}}
/>
<LiquidityPanel pool={pool} setPool={setPool} />
<Col>
<h2 className="my-2 text-xl">Limit Order</h2>
TODO
</Col>
<Col>
<h2 className="my-2 text-xl">Buy Shares</h2>
{/* <input className="input" placeholder="User" type="text" /> */}
<input
className="input"
placeholder="Amount"
type="number"
onChange={(e) => setBuyAmount(parseFloat(e.target.value))}
/>
<Row className="gap-2 py-2">
<div>Y shares purchaseable: {yesPurchased.toFixed(2)}</div>
<div>New Tick: {newPoolTick}</div>
<div>New prob: {formatPercent(toProb(newPoolTick))}</div>
</Row>
<Row className="gap-2">
<button className="btn">Buy YES</button>
{/* <button className="btn">Buy NO</button> */}
</Row>
</Col>
</Col>
)
}
function inputPercentToTick(event: React.ChangeEvent<HTMLInputElement>) {
return fromProb(parseFloat(event.target.value) / 100)
}