From 988d257086ba2a9e9922e6ae17316c864abe8725 Mon Sep 17 00:00:00 2001 From: defispartan Date: Tue, 21 Jan 2025 23:13:05 -0600 Subject: [PATCH] feat: init GHO reserve interest rate history components --- src/modules/history/gho-rate-history-query.ts | 10 + .../Gho/GhoReserveConfiguration.tsx | 10 + .../graphs/GHOInterestRateModelGraph.tsx | 434 ++++++++++++++++++ .../GHOInterestRateModelGraphContainer.tsx | 50 ++ 4 files changed, 504 insertions(+) create mode 100644 src/modules/history/gho-rate-history-query.ts create mode 100644 src/modules/reserve-overview/graphs/GHOInterestRateModelGraph.tsx create mode 100644 src/modules/reserve-overview/graphs/GHOInterestRateModelGraphContainer.tsx diff --git a/src/modules/history/gho-rate-history-query.ts b/src/modules/history/gho-rate-history-query.ts new file mode 100644 index 0000000000..7d2826a475 --- /dev/null +++ b/src/modules/history/gho-rate-history-query.ts @@ -0,0 +1,10 @@ +export const GHO_RATE_HISTORY_QUERY = ` +query GHORateHistory { + { + reserveConfigurationHistoryItems(where: {reserve_: {symbol: "GHO"}}){ + timestamp + reserveInterestRateStrategy + } + } +} +`; diff --git a/src/modules/reserve-overview/Gho/GhoReserveConfiguration.tsx b/src/modules/reserve-overview/Gho/GhoReserveConfiguration.tsx index e20a28ff8b..8030802b1c 100644 --- a/src/modules/reserve-overview/Gho/GhoReserveConfiguration.tsx +++ b/src/modules/reserve-overview/Gho/GhoReserveConfiguration.tsx @@ -8,6 +8,7 @@ import { ReserveEModePanel } from '../ReserveEModePanel'; import { PanelRow, PanelTitle } from '../ReservePanels'; import { GhoBorrowInfo } from './GhoBorrowInfo'; import { GhoDiscountCalculator } from './GhoDiscountCalculator'; +import { GHOInterestRateModelGraphContainer } from '../graphs/GHOInterestRateModelGraphContainer'; type GhoReserveConfigurationProps = { reserve: ComputedReserveData; @@ -93,6 +94,15 @@ export const GhoReserveConfiguration: React.FC = ( + + + + Historical Interest Rate + + + + + {reserve.eModes.length > 0 && ( <> diff --git a/src/modules/reserve-overview/graphs/GHOInterestRateModelGraph.tsx b/src/modules/reserve-overview/graphs/GHOInterestRateModelGraph.tsx new file mode 100644 index 0000000000..9319bb224c --- /dev/null +++ b/src/modules/reserve-overview/graphs/GHOInterestRateModelGraph.tsx @@ -0,0 +1,434 @@ +import { normalizeBN, RAY, rayDiv, rayMul } from '@aave/math-utils'; +import { Trans } from '@lingui/macro'; +import { Box, Typography, useMediaQuery, useTheme } from '@mui/material'; +import { AxisBottom, AxisLeft } from '@visx/axis'; +import { curveMonotoneX } from '@visx/curve'; +import { localPoint } from '@visx/event'; +import { GridRows } from '@visx/grid'; +import { Group } from '@visx/group'; +import { scaleLinear } from '@visx/scale'; +import { Bar, Line, LinePath } from '@visx/shape'; +import { Text } from '@visx/text'; +import { defaultStyles, TooltipWithBounds, withTooltip } from '@visx/tooltip'; +import { WithTooltipProvidedProps } from '@visx/tooltip/lib/enhancers/withTooltip'; +import { BigNumber } from 'bignumber.js'; +import { bisector, max } from 'd3-array'; +import React, { useCallback, useMemo } from 'react'; + +import type { Fields } from './InterestRateModelGraphContainer'; + +type TooltipData = Rate; + +type InterestRateModelType = { + variableRateSlope1: string; + variableRateSlope2: string; + optimalUsageRatio: string; + utilizationRate: string; + baseVariableBorrowRate: string; + totalLiquidityUSD: string; + totalDebtUSD: string; +}; + +type Rate = { + variableRate: number; + utilization: number; +}; + +// accessors +const getDate = (d: Rate) => d.utilization; +const bisectDate = bisector((d) => d.utilization).center; +const getVariableBorrowRate = (d: Rate) => d.variableRate * 100; +const tooltipValueAccessors = { + variableBorrowRate: getVariableBorrowRate, + utilizationRate: () => 38, +}; + +const resolution = 200; +const step = 100 / resolution; + +// const getAPY = (rate: BigNumber) => +// rayPow(valueToZDBigNumber(rate).dividedBy(SECONDS_PER_YEAR).plus(RAY), SECONDS_PER_YEAR).minus( +// RAY +// ); + +function getRates({ + variableRateSlope1, + variableRateSlope2, + optimalUsageRatio, + baseVariableBorrowRate, +}: InterestRateModelType): Rate[] { + const rates: Rate[] = []; + const formattedOptimalUtilizationRate = normalizeBN(optimalUsageRatio, 25).toNumber(); + + for (let i = 0; i <= resolution; i++) { + const utilization = i * step; + // When zero + if (utilization === 0) { + rates.push({ + variableRate: 0, + utilization, + }); + } + // When hovering below optimal utilization rate, actual data + else if (utilization < formattedOptimalUtilizationRate) { + const theoreticalVariableAPY = normalizeBN( + new BigNumber(baseVariableBorrowRate).plus( + rayDiv(rayMul(variableRateSlope1, normalizeBN(utilization, -25)), optimalUsageRatio) + ), + 27 + ).toNumber(); + rates.push({ + variableRate: theoreticalVariableAPY, + utilization, + }); + } + // When hovering above optimal utilization rate, hypothetical predictions + else { + const excess = rayDiv( + normalizeBN(utilization, -25).minus(optimalUsageRatio), + RAY.minus(optimalUsageRatio) + ); + const theoreticalVariableAPY = normalizeBN( + new BigNumber(baseVariableBorrowRate) + .plus(variableRateSlope1) + .plus(rayMul(variableRateSlope2, excess)), + 27 + ).toNumber(); + rates.push({ + variableRate: theoreticalVariableAPY, + utilization, + }); + } + } + return rates; +} + +export type AreaProps = { + width: number; + height: number; + margin?: { top: number; right: number; bottom: number; left: number }; + fields: Fields; + reserve: InterestRateModelType; +}; + +export const GHOInterestRateModelGraph = withTooltip( + ({ + width, + height, + margin = { top: 20, right: 10, bottom: 20, left: 40 }, + showTooltip, + hideTooltip, + tooltipData, + tooltipLeft = 0, + fields, + reserve, + }: AreaProps & WithTooltipProvidedProps) => { + if (width < 10) return null; + const theme = useTheme(); + + // Formatting + const formattedCurrentUtilizationRate = (parseFloat(reserve.utilizationRate) * 100).toFixed(2); + const formattedOptimalUtilizationRate = normalizeBN(reserve.optimalUsageRatio, 25).toNumber(); + + // Tooltip Styles + const accentColorDark = theme.palette.mode === 'light' ? '#383D511F' : '#a5a8b647'; + const tooltipStyles = { + ...defaultStyles, + padding: '8px 12px', + boxShadow: '0px 0px 2px rgba(0, 0, 0, 0.2), 0px 2px 10px rgba(0, 0, 0, 0.1)', + borderRadius: '4px', + color: '#62677B', + fontSize: '12px', + lineHeight: '16px', + letterSpacing: '0.15px', + }; + const tooltipStylesDark = { + ...tooltipStyles, + background: theme.palette.background.default, + }; + + const data = useMemo(() => getRates(reserve), [JSON.stringify(reserve)]); + + // bounds + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // scales + const dateScale = useMemo( + () => + scaleLinear({ + range: [0, innerWidth], + domain: [0, 100], + nice: true, + }), + [innerWidth] + ); + const yValueScale = useMemo(() => { + const maxY = max(data, (d) => getVariableBorrowRate(d)) as number; + return scaleLinear({ + range: [innerHeight, 0], + domain: [0, (maxY || 0) * 1.1], + nice: true, + }); + }, [innerHeight, data, reserve]); + + // tooltip handler + const handleTooltip = useCallback( + (event: React.TouchEvent | React.MouseEvent) => { + const { x: _x } = localPoint(event) || { x: 0 }; + const x = _x - margin.left; + const x0 = dateScale.invert(x); + const index = bisectDate(data, x0, 1); + const d0 = data[index - 1]; + const d1 = data[index]; + let d = d0; + if (d1 && getDate(d1)) { + d = x0.valueOf() - getDate(d0).valueOf() > getDate(d1).valueOf() - x0.valueOf() ? d1 : d0; + } + showTooltip({ + tooltipData: d, + tooltipLeft: x, + }); + }, + [showTooltip, dateScale, data, margin] + ); + + const ticks = [ + { + value: normalizeBN(reserve.optimalUsageRatio, 27).multipliedBy(100).toNumber(), + label: 'optimal', + }, + { + value: new BigNumber(reserve.utilizationRate).multipliedBy(100).toNumber(), + label: 'current', + }, + ]; + + const isMobile = useMediaQuery(theme.breakpoints.down('lg')); + + return ( + <> + + + {/* Horizontal Background Lines */} + + + {/* Variable Borrow APR Line */} + dateScale(getDate(d)) ?? 0} + y={(d) => yValueScale(getVariableBorrowRate(d)) ?? 0} + curve={curveMonotoneX} + /> + + {/* X Axis */} + ({ + fill: theme.palette.text.muted, + fontSize: 10, + textAnchor: 'middle', + })} + tickFormat={(n) => `${n}%`} + /> + + {/* Y Axis */} + ({ + fill: theme.palette.text.muted, + fontSize: 8, + dx: -margin.left + 10, + })} + numTicks={2} + tickFormat={(value) => `${value}%`} + /> + + {/* Background */} + hideTooltip()} + /> + + {/* Current Utilization Line */} + + + {`Current ${formattedCurrentUtilizationRate}%`} + + + {/* Optimal Utilization Line */} + + + {`Optimal ${formattedOptimalUtilizationRate}%`} + + + {/* Tooltip */} + {tooltipData && ( + + {/* Vertical line */} + + {/* Variable borrow rate circle */} + + + + )} + + + + {/* Tooltip Info */} + {tooltipData && ( +
+ + + + Utilization Rate + + + {tooltipData.utilization}% + + + + + {(tooltipData.utilization / 100) * parseFloat(reserve.totalLiquidityUSD) - + parseFloat(reserve.totalDebtUSD) > + 0 ? ( + <> + + Borrow amount to reach {tooltipData.utilization}% utilization + + + $ + {new Intl.NumberFormat('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format( + (tooltipData.utilization / 100) * parseFloat(reserve.totalLiquidityUSD) - + parseFloat(reserve.totalDebtUSD) + )} + + + ) : ( + <> + + + Repayment amount to reach {tooltipData.utilization}% utilization + + + + $ + {new Intl.NumberFormat('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format( + Math.abs( + (tooltipData.utilization / 100) * parseFloat(reserve.totalLiquidityUSD) - + parseFloat(reserve.totalDebtUSD) + ) + )} + + + )} + + + {fields.map((field) => ( + + + {field.text} + + + {tooltipValueAccessors[field.name](tooltipData).toFixed(2)}% + + + ))} + +
+ )} + + ); + } +); diff --git a/src/modules/reserve-overview/graphs/GHOInterestRateModelGraphContainer.tsx b/src/modules/reserve-overview/graphs/GHOInterestRateModelGraphContainer.tsx new file mode 100644 index 0000000000..41082b158c --- /dev/null +++ b/src/modules/reserve-overview/graphs/GHOInterestRateModelGraphContainer.tsx @@ -0,0 +1,50 @@ +import { Box } from '@mui/material'; +import { ParentSize } from '@visx/responsive'; +import { GraphLegend } from './GraphLegend'; +import { InterestRateModelGraph } from './InterestRateModelGraph'; +import { GHOInterestRateModelGraph } from './GHOInterestRateModelGraph'; + +export type Field = 'variableBorrowRate' | 'utilizationRate'; + +export type Fields = { name: Field; color: string; text: string }[]; + +// This graph takes in its data via props, thus having no loading/error states +export const GHOInterestRateModelGraphContainer = () => { + const CHART_HEIGHT = 155; + const fields: Fields = [ + { name: 'variableBorrowRate', text: 'Borrow APR, variable', color: '#B6509E' }, + ]; + + return ( + + + + + + {({ width }) => ( + + )} + + + ); +};