diff --git a/src/App.tsx b/src/App.tsx index 5964467..dbd38ca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,15 +15,27 @@ import SupplyVsDemandChart from "./SupplyVsDemandChart"; import ResultParams from "./ResultParams"; import PriceSimulationChart from "./PriceSimulationChart"; import HelpText from "./HelpText"; +// Text content +import { + parameterDescriptions, + simulationParameterDescriptions, + resultParameterDescriptions +} from "./parametersDescriptions"; // Utils import { getLast, getAvg, pause } from "./utils"; import { getInitialParams, getPriceR, + getMinPrice, + getS, + vest_tokens, + getR, getSlippage, getTxDistribution, getDeltaR_priceGrowth, - rv_U + rv_U, + getMedian, + getSum } from "./math"; import { throttle } from "lodash"; // General styles @@ -116,65 +128,17 @@ const useStyles = makeStyles((theme: Theme) => } }, descriptionTitle: { - fontWeight: theme.typography.fontWeightBold, padding: theme.spacing(0.5) }, - descriptionName: { - fontWeight: theme.typography.fontWeightBold + descriptionBody: { + color: "#dbdfe4" + }, + descriptionPadding: { + padding: theme.spacing(0.5) } }) ); -const parameterDescriptions = [ - { - name: "Initial raise", - text: "Total funds raised in the hatch period of the ABC launch" - }, - { - name: "Allocation to funding pool", - text: - "The percentage of the funds raised in the Hatch sale that go directly into the project funding pool to compensate future work done in the project" - }, - { - name: "Hatch price", - text: - "The price paid per 'ABC token' by community members involved in hatching the project" - }, - { - name: "Post-hatch price", - text: - "The price of the 'ABC token' when the curve enters the open phase and is live for public participation" - }, - { - name: "Exit tribute", - text: - "The percentage of funds that are diverted to the project funding pool from community members who exit funds from the project by burning 'ABC tokens' in exchange for collateral" - } -]; - -const resultParameterDescriptions = [ - { - name: "Total reserve", - text: - "Total DAI in the smart contract reserve at the end of the simulated period" - }, - { - name: "Funds generated from initial hatch", - text: - "Fraction of the funds (theta) raised during the hatch that go directly to the cause" - }, - { - name: "Funds generated from exit tributes", - text: - "Cumulative amount of exit tributes collected from only exit /sell transactions" - }, - { - name: "Average slippage", - text: - "Average of the slippage of each transaction occured during the simulation period" - } -]; - export default function App() { const [curveParams, setCurveParams] = useState({ theta: 0.35, // fraction allocated to reserve (.) @@ -199,7 +163,7 @@ export default function App() { const { k, // Invariant power kappa (.) R0, // Initial reserve (DAI) - // S0, // initial supply of tokens (token) + S0, // initial supply of tokens (token) V0 // invariant coef } = getInitialParams({ d0, @@ -210,6 +174,7 @@ export default function App() { const [priceTimeseries, setPriceTimeseries] = useState([0]); const [withdrawFeeTimeseries, setWithdrawFeeTimeseries] = useState([0]); + const [floorpriceTimeseries, setFloorpriceTimeseries] = useState([0]); const [totalReserve, setTotalReserve] = useState(R0); const [withdrawCount, setWithdrawCount] = useState(0); const [avgSlippage, setAvgSlippage] = useState(0); @@ -248,48 +213,100 @@ export default function App() { async function simulateRandomDelta() { const R_t: number[] = [R0]; + const S_t: number[] = [S0]; const p_t: number[] = [getPriceR({ R: R0, V0, k })]; const wFee_t: number[] = [0]; const slippage_t: number[] = []; const avgTxSize_t: number[] = []; + // hatchers tokens = S0[section added by Z] + const H_t: number[] = [S0]; // total hatcher tokens not vested + const floorprice_t: number[] = []; // initially the price is the floor as all tokens are hatcher tokens + // Random walk const numSteps = 52; + const u_min = 0.97; + const u_max = 1.04; + const tx_spread = 10; + // vesting(should this be exposed in the app ?) + const cliff = 8; // weeks before vesting starts ~2 months + const halflife = 52; // 26 weeks, half life is ~6 months + // percentage of the hatch tokens which vest per week(since that is our timescale in the sim) // numSteps = 52 take 8ms to run setSimulationRunning(true); for (let t = 0; t < numSteps; t++) { - const txsWeek = Math.ceil(t < 5 ? rv_U(0, 5) : rv_U(5, 2 * t)); - const priceGrowth = rv_U(0.99, 1.03); + const txsWeek = rv_U(5, 2 * t + 5); const R = getLast(R_t); - const deltaR = getDeltaR_priceGrowth({ R, k, priceGrowth }); + const S = getLast(S_t); + const H = getLast(H_t); + // enforce the effects of the unvested tokens not being burnable + let u_lower; + if (H > S) { + u_lower = 1; + } else { + // compute the reserve if all that supply is burned + const R_ratio = getR({ S: S - H, V0, k }) / R; + u_lower = Math.max(1 - R_ratio, u_min); + } + const priceGrowth = rv_U(u_lower, u_max); + + const deltaR = getDeltaR_priceGrowth({ R, k, priceGrowth }); const R_next = R + deltaR; - const txs = getTxDistribution({ sum: deltaR, num: txsWeek }); + const txs = getTxDistribution({ + sum: deltaR, + num: txsWeek, + spread: tx_spread + }); // Compute slippage - const slippage = getAvg( - txs.map(txR => getSlippage({ R, deltaR: txR, V0, k })) + const slippage_txs = txs.map(txR => + getSlippage({ R, deltaR: txR, V0, k }) ); + const slippage = getMedian(slippage_txs); + const txsWithdraw = txs.filter(tx => tx < 0); - const wFees = -wFee * txsWithdraw.reduce((t, c) => t + c, 0); - const _avgTxSize = - txs.reduce((t, c) => t + Math.abs(c), 0) / txs.length; + const wFees = -wFee * getSum(txsWithdraw); + // txsWithdraw.reduce((t, c) => t + c, 0); + + // Vest + const delta_H = vest_tokens({ week: t, H, halflife, cliff }); + const H_next = H - delta_H; + + // find floor price + const S_next = getS({ R, V0, k }); + const floorprice_next = getMinPrice({ + S: S_next, + H: S0 - H_next, + V0, + k + }); + + const _avgTxSize = getMedian(txsWithdraw); R_t.push(R_next); + S_t.push(S_next); + H_t.push(H_next); p_t.push(getPriceR({ R: R_next, V0, k })); slippage_t.push(slippage); avgTxSize_t.push(_avgTxSize); wFee_t.push(getLast(wFee_t) + wFees); + + floorprice_t.push(floorprice_next); setWithdrawCount(c => c + txsWithdraw.length); // Stop the simulation if it's no longer active if (!simulationActive || !canContinueSimulation) break; } + // floorprice_t is missing one data point + floorprice_t[floorprice_t.length] = floorprice_t[floorprice_t.length - 1]; + setPriceTimeseries(p_t); setWithdrawFeeTimeseries(wFee_t); + setFloorpriceTimeseries(floorprice_t); setAvgSlippage(getAvg(slippage_t)); setAvgTxSize(getAvg(avgTxSize_t)); setTotalReserve(getLast(R_t)); @@ -307,14 +324,17 @@ export default function App() { const resultFields = [ { label: `Total reserve`, + description: resultParameterDescriptions.totalReserve.text, value: (+totalReserve.toPrecision(3)).toLocaleString() + " DAI" }, { label: `Funds generated from initial hatch`, + description: resultParameterDescriptions.initialHatchFunds.text, value: Math.round(d0 * theta).toLocaleString() + " DAI" }, { label: `Funds generated from exit tributes (${withdrawCount} txs)`, + description: resultParameterDescriptions.exitTributes.text, value: (+getLast(withdrawFeeTimeseries).toPrecision(3)).toLocaleString() + " DAI" @@ -323,6 +343,7 @@ export default function App() { label: `Average slippage (avg tx size ${Math.round( avgTxSize ).toLocaleString()} DAI)`, + description: resultParameterDescriptions.slippage.text, value: +(100 * avgSlippage).toFixed(3) + "%" } ]; @@ -353,15 +374,21 @@ export default function App() { - {parameterDescriptions.map(({ name, text }) => ( + {[ + parameterDescriptions.theta, + parameterDescriptions.p0, + parameterDescriptions.p1, + parameterDescriptions.wFee, + parameterDescriptions.d0 + ].map(({ name, text }) => ( ))} @@ -398,13 +425,16 @@ export default function App() { Preview - Visualization of the token bonding curve analytic function - on a specific range of reserve [0, 4 * R0]. This result is - deterministic given the current set of parameters and will - never change regardes of the campaign performance, it only - shows how the price will react to reserve changes. - +
+ + Visualization of the token bonding curve analytic + function on a specific range of reserve [0, 4 * R0]. + This result is deterministic given the current set of + parameters and will never change regardes of the + campaign performance, it only shows how the price will + react to reserve changes. + +
} /> @@ -449,14 +479,40 @@ export default function App() { Simulation - This chart shows a 52 week simulation of discrete - transactions interacting with the token bonding curve. - Each transaction adds or substract reserve to the - system, modifying the price over time. The frequency, - size and direction of each transaction is computed - from a set of bounded random functions. - +
+
+ + This chart shows a 52 week simulation of discrete + transactions interacting with the token bonding + curve. Each transaction adds or substract reserve + to the system, modifying the price over time. The + frequency, size and direction of each transaction + is computed from a set of bounded random + functions. + +
+ +
- - {name} - + {name} - {text} + + {text} +
+ + {Object.values( + simulationParameterDescriptions + ).map(({ name, text }) => ( + + + + + ))} + +
+ {name} + + + {text} + +
+ } /> @@ -465,6 +521,7 @@ export default function App() { @@ -486,18 +543,18 @@ export default function App() { - {resultParameterDescriptions.map( + {Object.values(resultParameterDescriptions).map( ({ name, text }) => ( ) diff --git a/src/CurveDesignInputParams.tsx b/src/CurveDesignInputParams.tsx index d0d9134..e9f7298 100644 --- a/src/CurveDesignInputParams.tsx +++ b/src/CurveDesignInputParams.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { InputFieldInterface, CurveParamsInterface } from "./types"; import InputParams from "./InputParams"; +import { parameterDescriptions } from "./parametersDescriptions"; export default function CurveDesignInputParams({ curveParams, @@ -42,6 +43,7 @@ export default function CurveDesignInputParams({ const inputFields: InputFieldInterface[] = [ { label: "Allocation to funding pool", + description: parameterDescriptions.theta.text, value: theta, setter: setTheta, min: 0, @@ -54,6 +56,7 @@ export default function CurveDesignInputParams({ }, { label: "Hatch price (DAI/token)", + description: parameterDescriptions.p0.text, value: p0, setter: _setP0, min: 0.01, @@ -65,6 +68,7 @@ export default function CurveDesignInputParams({ }, { label: "Post-hatch price (DAI/token)", + description: parameterDescriptions.p1.text, value: p1, setter: setP1, min: p0 || 0.1, @@ -76,6 +80,7 @@ export default function CurveDesignInputParams({ }, { label: "Exit tribute", + description: parameterDescriptions.wFee.text, value: wFee, setter: setWFee, min: 0, diff --git a/src/InputParams.tsx b/src/InputParams.tsx index fd9f603..3d06aaa 100644 --- a/src/InputParams.tsx +++ b/src/InputParams.tsx @@ -1,11 +1,11 @@ import React from "react"; import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; import Grid from "@material-ui/core/Grid"; import TextField from "@material-ui/core/TextField"; import NumberFormat from "react-number-format"; import { InputFieldInterface } from "./types"; import PrettoSlider from "./PrettoSlider"; +import TextWithPopover from "./TextWithPopover"; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -81,6 +81,7 @@ export default function InputParams({ {inputFields.map( ({ label, + description, value, setter, min, @@ -103,9 +104,7 @@ export default function InputParams({ return ( - - {label} - + diff --git a/src/PriceSimulationChart.tsx b/src/PriceSimulationChart.tsx index bcc0e40..df5ab1e 100644 --- a/src/PriceSimulationChart.tsx +++ b/src/PriceSimulationChart.tsx @@ -17,6 +17,7 @@ import { linspace } from "./utils"; const keyHorizontal = "x"; const keyVerticalLeft = "Price (DAI/token)"; const keyVerticalRight = "Total exit tributes (DAI)"; +const keyVerticalLeft2 = "Floor price (DAI/token)"; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -32,11 +33,13 @@ const useStyles = makeStyles((theme: Theme) => function PriceSimulationChart({ priceTimeseries, withdrawFeeTimeseries, + floorpriceTimeseries, p0, p1 }: { priceTimeseries: number[]; withdrawFeeTimeseries: number[]; + floorpriceTimeseries: number[]; p0: number; p1: number; }) { @@ -51,6 +54,7 @@ function PriceSimulationChart({ data.push({ [keyHorizontal]: t, [keyVerticalLeft]: priceTimeseries[t] || 0, + [keyVerticalLeft2]: floorpriceTimeseries[t] || 0, [keyVerticalRight]: withdrawFeeTimeseries[t] || 0 }); } @@ -70,8 +74,8 @@ function PriceSimulationChart({ const { textAnchor, viewBox, text } = props; return ( @@ -83,10 +87,12 @@ function PriceSimulationChart({ function CustomTooltip({ active, payload, label }: any) { if (active) { const price = payload[0].value; - const exit = payload[1].value; + const floor = payload[1].value; + const exit = payload[2].value; const weekNum = label; const toolTipData: string[][] = [ ["Price", price.toFixed(2), "DAI/tk"], + ["Floor", floor.toFixed(2), "DAI/tk"], ["Exit t.", formatter(exit), "DAI"], ["Week", weekNum, ""] ]; @@ -148,12 +154,10 @@ function PriceSimulationChart({ {/* Capital collected from withdraw fees - AXIS */} @@ -166,7 +170,20 @@ function PriceSimulationChart({ dataKey={keyVerticalLeft} stroke={theme.palette.primary.main} fill={theme.palette.primary.main} + fillOpacity={0.3} + strokeWidth={2} /> + + {/* createStyles({ @@ -45,6 +46,7 @@ export default function ResultParams({ }: { resultFields: { label: string; + description: string; value: number | string; }[]; }) { @@ -52,12 +54,10 @@ export default function ResultParams({ return (
- {resultFields.map(({ label, value }) => ( + {resultFields.map(({ label, description, value }) => ( - - {label} - + diff --git a/src/SimulationInputParams.tsx b/src/SimulationInputParams.tsx index e02f36e..93dcc55 100644 --- a/src/SimulationInputParams.tsx +++ b/src/SimulationInputParams.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { InputFieldInterface, CurveParamsInterface } from "./types"; import InputParams from "./InputParams"; +import { parameterDescriptions } from "./parametersDescriptions"; export default function CurveDesignInputParams({ curveParams, @@ -25,6 +26,7 @@ export default function CurveDesignInputParams({ const inputFields: InputFieldInterface[] = [ { label: "Initial raise (DAI)", + description: parameterDescriptions.d0.text, value: d0, setter: setD0, min: 0.1e6, diff --git a/src/SupplyVsDemandChart.tsx b/src/SupplyVsDemandChart.tsx index 54c76a7..85e3d1b 100644 --- a/src/SupplyVsDemandChart.tsx +++ b/src/SupplyVsDemandChart.tsx @@ -186,6 +186,8 @@ function SupplyVsDemandChart({ dataKey={keyVertical} stroke={theme.palette.primary.main} fill={theme.palette.primary.main} + fillOpacity={0.3} + strokeWidth={2} /> ({ + container: { + color: theme.palette.text.secondary, + display: "flex", + marginLeft: "6px", + fontSize: "0.9rem", + cursor: "pointer", + transition: "color ease 150ms", + "&:hover": { + color: "#c3c9d0" + } + }, + popoverContainer: { + padding: theme.spacing(2), + "& > p:not(:last-child)": { + paddingBottom: theme.spacing(1), + marginBottom: theme.spacing(1), + borderBottom: "1px solid #3f5463" + } + }, + paper: { + backgroundColor: "#384b59", + maxWidth: theme.breakpoints.values.md * 0.9, + [`@media screen and (max-width: ${theme.breakpoints.values.md}px)`]: { + maxWidth: "90vw" + }, + padding: theme.spacing(0.5) + }, + descriptionBody: { + color: "#dbdfe4" + } +})); + +export default function TextWithPopover({ + content, + popoverText +}: { + content: string; + popoverText: string; +}) { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = React.useState(null); + + function handleClick(event: any) { + setAnchorEl(event.currentTarget); + } + + function handleClose() { + setAnchorEl(null); + } + + const open = Boolean(anchorEl); + const id = open ? "simple-popover" : undefined; + + return ( +
+
+ {content} +
+ + + {content} + + {popoverText} + + + +
+ ); +} diff --git a/src/math.ts b/src/math.ts index 94050a4..4a054bd 100644 --- a/src/math.ts +++ b/src/math.ts @@ -23,6 +23,37 @@ export function getInitialParams({ return { k, R0, S0, V0 }; } +export function getR({ S, V0, k }: { S: number; V0: number; k: number }) { + return S ** k / V0; +} + +export function getS({ R, V0, k }: { R: number; V0: number; k: number }) { + return (V0 * R) ** (1 / k); +} + +// compute the price if all that supply is burned +export function getMinPrice({ + S, + H, + V0, + k +}: { + S: number; + H: number; + V0: number; + k: number; +}) { + if (S === H) { + const myR = getR({ S, V0, k }); + const myP = getPriceR({ R: myR, V0, k }); // numerical precision make complex numbers just suppress it + return Math.abs(myP); + } else { + // compute the reserve if all that supply is burned + const minR = getR({ S: S - H, V0, k }); + return getPriceR({ R: minR, V0, k }); + } +} + /** * Computes the price at a specific reserve `R` */ @@ -75,17 +106,47 @@ export function getDeltaR_priceGrowth({ * Demo: https://codepen.io/anon/pen/mNqJjv?editors=0010#0 * Very quick: < 10ms for 10000 txs */ -export function getTxDistribution({ sum, num }: { sum: number; num: number }) { +export function getTxDistribution({ + sum, + num, + spread +}: { + sum: number; + num: number; + spread: number; +}) { const mean = sum / num; - const off = mean * 4; + const off = mean * spread; const x: number[] = []; for (let i = 0; i < num; i++) { - x[i] = randn_bm(mean - off, mean + off); + x.push(randn_bm(mean - off, mean + off)); } return x; } -// Minor utils +export function vest_tokens({ + week, + H, // unvested_hatch_tokens + halflife, + cliff +}: { + week: number; + H: number; + halflife: number; + cliff: number; +}) { + // check cliff + if (week < cliff) { + return 0; + } else { + // rate of release given half - life + const vest_fraction = 0.5 ** (1 / halflife); + // number of tokens that vest in this week + return H * (1 - vest_fraction); + } +} + +// Statistics utils /** * Random variable uniformly distributed @@ -111,3 +172,15 @@ function randn_bm(min: number, max: number) { num += min; // offset to min return num; } + +// Array utils + +export function getMedian(arr: number[]) { + const mid = Math.floor(arr.length / 2); + const nums = [...arr].sort((a, b) => a - b); + return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2; +} + +export function getSum(arr: number[]) { + return arr.reduce((a, b) => a + b, 0); +} diff --git a/src/parametersDescriptions.ts b/src/parametersDescriptions.ts new file mode 100644 index 0000000..0b7ebf7 --- /dev/null +++ b/src/parametersDescriptions.ts @@ -0,0 +1,69 @@ +export interface DescriptionObject { + [key: string]: { name: string; text: string }; +} + +export const parameterDescriptions: DescriptionObject = { + theta: { + name: "Allocation to funding pool", + text: + "The percentage of the funds raised in the Hatch sale that go directly into the project funding pool to compensate future work done in the project" + }, + p0: { + name: "Hatch price", + text: + "The price paid per 'ABC token' by community members involved in hatching the project" + }, + p1: { + name: "Post-hatch price", + text: + "The price of the 'ABC token' when the curve enters the open phase and is live for public participation" + }, + wFee: { + name: "Exit tribute", + text: + "The percentage of funds that are diverted to the project funding pool from community members who exit funds from the project by burning 'ABC tokens' in exchange for collateral" + }, + d0: { + name: "Initial raise", + text: "Total funds raised in the hatch period of the ABC launch" + } +}; + +export const simulationParameterDescriptions: DescriptionObject = { + price: { + name: "Price", + text: "Price of the token over time." + }, + floorPrice: { + name: "Floor price", + text: + "Lower bound of the price guaranteed by the vesting of hatch tokens. It decreases over time as more hatch tokens are allowed to be traded" + }, + exitTributes: { + name: "Total exit tributes", + text: + "Cumulative sum of exit tributes collected from only exit /sell transactions" + } +}; + +export const resultParameterDescriptions: DescriptionObject = { + totalReserve: { + name: "Total reserve", + text: + "Total DAI in the smart contract reserve at the end of the simulated period" + }, + initialHatchFunds: { + name: "Funds generated from initial hatch", + text: + "Fraction of the funds (theta) raised during the hatch that go directly to the cause (analytic result)" + }, + exitTributes: { + name: "Funds generated from exit tributes", + text: simulationParameterDescriptions.exitTributes.text + }, + slippage: { + name: "Average slippage", + text: + "Average of the slippage of each transaction occured during the simulation period" + } +}; diff --git a/src/types.ts b/src/types.ts index 2fcc5e2..e0f426c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ export interface InputFieldInterface { label: string; + description: string; value: number; setter(newValue: any): void; min: number;
- - {name} - + {name} - {text} + + {text} +