augmented-tbc-design/src/App.tsx

591 lines
19 KiB
TypeScript

import React, { useState, useEffect, useMemo } from "react";
// Material UI
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
import Typography from "@material-ui/core/Typography";
import Box from "@material-ui/core/Box";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
// Components
import Header from "./Header";
import CurveDesignInputParams from "./CurveDesignInputParams";
import SimulationInputParams from "./SimulationInputParams";
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,
getMedian,
getSum
} from "./math";
import { throttle } from "lodash";
// General styles
import "./app.css";
const headerOffset = 10;
const useStyles = makeStyles((theme: Theme) =>
createStyles({
mainContainer: {
"& > div:not(:last-child)": {
paddingBottom: theme.spacing(3)
},
"& > div": {
"& > div": {
paddingTop: "0 !important"
}
},
paddingBottom: theme.spacing(9)
},
simulationContainer: {
minHeight: "442px"
},
paper: {
width: "100%",
height: "100%",
minHeight: 310,
backgroundColor: "#293640"
},
box: {
padding: theme.spacing(3, 3)
},
boxButton: {
padding: theme.spacing(3, 3)
},
boxHeader: {
padding: theme.spacing(3, 3),
height: theme.spacing(headerOffset),
display: "flex",
alignItems: "center",
borderBottom: "1px solid #313d47"
},
boxBorderBottom: {
borderBottom: "1px solid #313d47"
},
initialRaise: {
justifyContent: "space-between"
},
boxChart: {
width: "100%",
height: "100%",
minHeight: 310,
maxHeight: 350,
padding: theme.spacing(3, 3),
// Correct the chart excessive margins
paddingRight: "5px",
paddingLeft: "5px"
},
boxPlaceholder: {
padding: theme.spacing(3, 3),
display: "flex",
height: "100%",
alignItems: "center",
justifyContent: "center",
color: theme.palette.text.secondary,
opacity: 0.4
},
header: {
backgroundColor: "#0b1216",
color: "#f8f8f8",
textAlign: "center",
padding: theme.spacing(3, 0, 6 + headerOffset),
marginBottom: -theme.spacing(headerOffset)
},
button: {
// background: "linear-gradient(290deg, #2ad179, #4ab47c)", // Green gradient
background: "linear-gradient(290deg, #1880e0, #3873d8)", // blue gradient
color: "white"
},
// Descriptions
descriptionContainer: {
"& > div:not(:last-child)": {
paddingBottom: theme.spacing(1),
marginBottom: theme.spacing(1),
borderBottom: "1px solid #3f5463"
},
"& td": {
verticalAlign: "top",
padding: theme.spacing(0.5)
}
},
descriptionTitle: {
padding: theme.spacing(0.5)
},
descriptionBody: {
color: "#dbdfe4"
},
descriptionPadding: {
padding: theme.spacing(0.5)
}
})
);
export default function App() {
const [curveParams, setCurveParams] = useState({
theta: 0.35, // fraction allocated to reserve (.)
p0: 0.1, // Hatch sale price p0 (DAI / token)
p1: 0.3, // Return factor (.)
wFee: 0.05, // friction coefficient (.)
d0: 3e6 // Initial raise, d0 (DAI)
});
const { d0, theta, p0, p1, wFee } = curveParams;
/**
* Throttle the curve update to prevent the expensive chart
* to re-render too often
*/
const setCurveParamsThrottle = useMemo(
() => throttle(setCurveParams, 250),
[]
);
// Simulation results
const {
k, // Invariant power kappa (.)
R0, // Initial reserve (DAI)
S0, // initial supply of tokens (token)
V0 // invariant coef
} = getInitialParams({
d0,
theta,
p0,
p1
});
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);
const [avgTxSize, setAvgTxSize] = useState(0);
// Simulation state variables
const [simulationActive, setSimulationActive] = useState(false);
const [simulationRunning, setSimulationRunning] = useState(false);
useEffect(() => {
setSimulationActive(false);
}, [curveParams]);
// #### TEST: Immediate simulation
async function startSimulation() {
// If there's a simulation already active, clear it
clearSimulation();
await pause(0);
// Start simulation by setting it to active
setSimulationActive(true);
}
function clearSimulation() {
// Stop simulation
setSimulationActive(false);
// Clear simulation variables
setWithdrawCount(0);
setPriceTimeseries([0]);
setWithdrawFeeTimeseries([0]);
setAvgSlippage(0);
}
useEffect(() => {
let canContinueSimulation = true;
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 = rv_U(5, 2 * t + 5);
const R = getLast(R_t);
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,
spread: tx_spread
});
// Compute slippage
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 * 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: S_next - 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));
setSimulationRunning(false);
}
if (simulationActive) simulateRandomDelta();
// Return an "unsubscribe" function that halts the run
return () => {
canContinueSimulation = false;
};
}, [simulationActive]);
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"
},
{
label: `Average slippage (avg tx size ${Math.round(
avgTxSize
).toLocaleString()} DAI)`,
description: resultParameterDescriptions.slippage.text,
value: +(100 * avgSlippage).toFixed(3) + "%"
}
];
const classes = useStyles();
return (
<>
<header className={classes.header}>
<Container fixed>
<Header />
</Container>
</header>
<Container fixed className={classes.mainContainer}>
<Grid container spacing={3}>
<Grid item xs={12} sm={12} md={6} lg={4}>
<Paper className={classes.paper}>
<Box className={classes.boxHeader}>
<Typography variant="h6">Curve Design</Typography>
<HelpText
text={
<div className={classes.descriptionContainer}>
<div>
<Typography className={classes.descriptionTitle}>
Parameters description:
</Typography>
</div>
<table>
<tbody>
{[
parameterDescriptions.theta,
parameterDescriptions.p0,
parameterDescriptions.p1,
parameterDescriptions.wFee,
parameterDescriptions.d0
].map(({ name, text }) => (
<tr key={name}>
<td>
<Typography>{name}</Typography>
</td>
<td>
<Typography className={classes.descriptionBody}>
{text}
</Typography>
</td>
</tr>
))}
</tbody>
</table>
</div>
}
/>
</Box>
<Box className={`${classes.box} ${classes.boxBorderBottom}`}>
<CurveDesignInputParams
curveParams={curveParams}
setCurveParams={setCurveParamsThrottle}
/>
</Box>
<Box className={`${classes.boxHeader} ${classes.initialRaise}`}>
<Typography variant="h6">Run parameters</Typography>
</Box>
<Box className={classes.box}>
<SimulationInputParams
curveParams={curveParams}
setCurveParams={setCurveParamsThrottle}
/>
</Box>
</Paper>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={8}>
<Paper className={classes.paper}>
<Box className={classes.boxHeader}>
<Typography variant="h6">Preview</Typography>
<HelpText
text={
<div className={classes.descriptionPadding}>
<Typography className={classes.descriptionBody}>
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.
</Typography>
</div>
}
/>
</Box>
<Box className={classes.boxChart}>
<SupplyVsDemandChart theta={theta} d0={d0} p0={p0} p1={p1} />
</Box>
</Paper>
</Grid>
</Grid>
<Grid container spacing={3}>
<Grid item xs={12} md={12}>
<Paper>
<Box className={classes.boxHeader}>
<Grid
container
direction="row"
justify="center"
alignItems="center"
>
<Button
variant="contained"
className={classes.button}
onClick={startSimulation}
disabled={simulationRunning}
>
Run simulation
</Button>
</Grid>
</Box>
</Paper>
</Grid>
</Grid>
<Grid container spacing={3} className={classes.simulationContainer}>
{simulationActive ? (
<>
<Grid item xs={12} sm={12} md={6} lg={8}>
<Paper className={classes.paper}>
<Box className={classes.boxHeader}>
<Typography variant="h6">Simulation</Typography>
<HelpText
text={
<div className={classes.descriptionContainer}>
<div className={classes.descriptionPadding}>
<Typography className={classes.descriptionBody}>
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.
</Typography>
</div>
<table>
<tbody>
{Object.values(
simulationParameterDescriptions
).map(({ name, text }) => (
<tr key={name}>
<td>
<Typography>{name}</Typography>
</td>
<td>
<Typography
className={classes.descriptionBody}
>
{text}
</Typography>
</td>
</tr>
))}
</tbody>
</table>
</div>
}
/>
</Box>
<Box className={classes.boxChart}>
<PriceSimulationChart
priceTimeseries={priceTimeseries}
withdrawFeeTimeseries={withdrawFeeTimeseries}
floorpriceTimeseries={floorpriceTimeseries}
p0={p0}
p1={p1}
/>
</Box>
</Paper>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={4}>
<Paper className={classes.paper}>
<Box className={classes.boxHeader}>
<Typography variant="h6">Results</Typography>
<HelpText
text={
<div className={classes.descriptionContainer}>
<div>
<Typography className={classes.descriptionTitle}>
Result parameters description:
</Typography>
</div>
<table>
<tbody>
{Object.values(resultParameterDescriptions).map(
({ name, text }) => (
<tr key={name}>
<td>
<Typography>{name}</Typography>
</td>
<td>
<Typography
className={classes.descriptionBody}
>
{text}
</Typography>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
}
/>
</Box>
<Box className={classes.box}>
<ResultParams resultFields={resultFields} />
</Box>
</Paper>
</Grid>
</>
) : (
<Grid item xs={12}>
<Paper className={classes.paper}>
<Box className={classes.boxPlaceholder}>
<Typography variant="h6">
Run a simulation to see results
</Typography>
</Box>
</Paper>
</Grid>
)}
</Grid>
</Container>
</>
);
}