augmented-tbc-design/src/App.tsx

555 lines
17 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,
supplyVsDemandChartDescription,
simulationChartDescription
} 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";
// Data
import { u_min_t, u_max_t } from "./u_values";
// General styles
import "./app.css";
const headerOffset = 10;
const simulationDuration = 4000;
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, #1aa059, #3d9567)", // Darker 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)
},
d0Container: {
"& > div": {
padding: "0 12px 0 0 !important",
display: "flex",
alignItems: "center"
}
},
d0Number: {
padding: "0 !important",
display: "flex",
alignItems: "center"
},
d0Slidder: {
padding: "0 12px 0 0 !important",
display: "flex",
alignItems: "center"
}
})
);
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 (.)
vHalflife: 17, // Vesting half life (weeks)
d0: 3e6 // Initial raise, d0 (DAI)
});
const { d0, theta, p0, p1, wFee, vHalflife } = 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(100, 40 * t + 100);
const R = getLast(R_t);
const S = getLast(S_t);
const H = getLast(H_t);
let R_next: number = 0,
S_next: number = 0,
H_next: number = 0,
price_next: number = 0,
txsWithdraw: number[] = [0],
floorprice_next: number = 1,
slippage: number = 0;
// Run the value compution again if the price goes below the floor price
/**
* Since the values u_min and u_max are predefined, it's possible that
* the price becomes less than the floor price. This cannot happen.
* So the next loop has 10 opportunities to find a price greater than
* the floor price. The for loop is used to prevent a possible infinite
* loop if a `while` loop was used.
*/
for (let i = 0; i < 20 && price_next < floorprice_next; i++) {
// enforce the effects of the unvested tokens not being burnable
let u_lower: number, u_upper: number;
// 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);
// }
// let priceGrowth = rv_U(u_lower, u_max);
u_lower = u_min_t[t];
u_upper = u_max_t[t];
if (i > 15) u_lower = 1;
const priceGrowth = rv_U(u_lower, u_upper);
const deltaR = getDeltaR_priceGrowth({ R, k, priceGrowth });
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 })
);
slippage = getMedian(slippage_txs);
txsWithdraw = txs.filter(tx => tx < 0);
// Vest
const delta_H = vest_tokens({
week: t,
H,
halflife: vHalflife,
cliff
});
H_next = H - delta_H;
// find floor price
S_next = getS({ R, V0, k });
floorprice_next = getMinPrice({
S: S_next,
H: S_next - H_next,
V0,
k
});
price_next = getPriceR({ R: R_next, V0, k });
}
const _avgTxSize = getMedian(txsWithdraw);
const wFees = -wFee * getSum(txsWithdraw);
R_t.push(R_next);
S_t.push(S_next);
H_t.push(H_next);
p_t.push(price_next);
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.filter(n => !isNaN(n))));
setTotalReserve(getLast(R_t));
setSimulationRunning(false);
}
if (simulationActive) simulateRandomDelta();
// Return an "unsubscribe" function that halts the run
return () => {
canContinueSimulation = false;
};
}, [simulationActive]);
// End results computed for chart visualization
const initialHatchFunds = d0 * theta;
const totalFundsRaisedTimeseries = withdrawFeeTimeseries.map(
x => x + initialHatchFunds
);
const resultFields = [
{
label: resultParameterDescriptions.totalReserve.name,
description: resultParameterDescriptions.totalReserve.text,
value: (+totalReserve.toPrecision(3)).toLocaleString() + " DAI"
},
{
label: resultParameterDescriptions.initialHatchFunds.name,
description: resultParameterDescriptions.initialHatchFunds.text,
value: Math.round(d0 * theta).toLocaleString() + " DAI"
},
{
label: resultParameterDescriptions.exitTributes.name,
description: resultParameterDescriptions.exitTributes.text,
value:
(+getLast(withdrawFeeTimeseries).toPrecision(3)).toLocaleString() +
" DAI",
valueFooter: `From ${withdrawCount} exit txs`
},
{
label: resultParameterDescriptions.slippage.name,
description: resultParameterDescriptions.slippage.text,
value: +(100 * avgSlippage).toFixed(3) + " %",
valueFooter: `Avg tx size ${Math.round(avgTxSize).toLocaleString()} DAI`
}
];
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
title={"Parameters description"}
table={[
parameterDescriptions.theta,
parameterDescriptions.p0,
parameterDescriptions.p1,
parameterDescriptions.wFee,
parameterDescriptions.vHalflife
]}
/>
</Box>
<Box className={`${classes.box} ${classes.boxBorderBottom}`}>
<CurveDesignInputParams
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 body={supplyVsDemandChartDescription} />
</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}>
<Paper>
<Box className={`${classes.box} ${classes.boxBorderBottom}`}>
<SimulationInputParams
curveParams={curveParams}
setCurveParams={setCurveParamsThrottle}
/>
</Box>
<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
body={simulationChartDescription}
table={Object.values(simulationParameterDescriptions)}
/>
</Box>
<Box className={classes.boxChart}>
<PriceSimulationChart
priceTimeseries={priceTimeseries}
floorpriceTimeseries={floorpriceTimeseries}
totalFundsRaisedTimeseries={totalFundsRaisedTimeseries}
simulationDuration={simulationDuration}
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
title={"Result parameters description"}
table={Object.values(resultParameterDescriptions)}
/>
</Box>
<Box className={classes.box}>
<ResultParams
resultFields={resultFields}
simulationDuration={simulationDuration}
/>
</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>
</>
);
}