import { useEffect, useMemo, useState } from 'react' import dayjs from 'dayjs' import { useTranslation } from 'next-i18next' import { LineChart, XAxis, YAxis, Line, Tooltip } from 'recharts' import useMangoStore from '../../stores/useMangoStore' import { numberCompactFormatter } from '../../utils/' import { exportDataToCSV } from '../../utils/export' import Button from '../Button' import useDimensions from 'react-cool-dimensions' import Select from 'components/Select' import Checkbox from 'components/Checkbox' import ButtonGroup from 'components/ButtonGroup' import * as MonoIcons from '../icons' import { SaveIcon, QuestionMarkCircleIcon } from '@heroicons/react/solid' import { useTheme } from 'next-themes' import { CHART_COLORS } from './LongShortChart' const utc = require('dayjs/plugin/utc') dayjs.extend(utc) export const handleDustTicks = (v) => { return v < 0.000001 ? v === 0 ? 0 : v.toExponential() : numberCompactFormatter.format(v) } const HEADERS = [ 'time', 'symbol', 'spot_value', 'perp_value', 'open_orders_value', 'transfer_balance', 'perp_spot_transfers_balance', 'mngo_rewards_value', 'mngo_rewards_quantity', 'long_funding', 'short_funding', 'long_funding_cumulative', 'short_funding_cumulative', // 'deposit_interest', // 'borrow_interest', // 'deposit_interest_cumulative', // 'borrow_interest_cumulative', // 'price', ] const DATA_CATEGORIES = [ 'account-value', 'account-pnl', 'perp-pnl', 'maker-volume', 'taker-volume', // 'interest-cumulative', 'funding-cumulative', 'mngo-rewards', ] const performanceRangePresets = [ { label: '24h', value: '1' }, { label: '7d', value: '7' }, { label: '30d', value: '30' }, { label: '3m', value: '90' }, ] const performanceRangePresetLabels = performanceRangePresets.map((x) => x.label) const performanceRangePresetValues = performanceRangePresets.map((x) => x.value) const AccountPerformance = () => { const { t } = useTranslation(['common', 'account-performance']) const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current) const [hourlyPerformanceStats, setHourlyPerformanceStats] = useState([]) const [uniqueSymbols, setUniqueSymbols] = useState([]) const [filteredSymbols, setFilteredSymbols] = useState([]) const [chartData, setChartData] = useState([]) const [loading, setLoading] = useState(false) const [selectedSymbols, setSelectedSymbols] = useState(['All']) const [chartToShow, setChartToShow] = useState('account-value') const [selectAll, setSelectAll] = useState(false) const [performanceRange, setPerformanceRange] = useState('30') const [volumeData, setVolumeData] = useState([]) const [volumeSymbols, setVolumeSymbols] = useState([]) const { observe, width, height } = useDimensions() const { theme } = useTheme() const mangoAccountPk = useMemo(() => { if (mangoAccount) { return mangoAccount.publicKey.toString() } }, [mangoAccount]) const exportPerformanceDataToCSV = () => { const dataToExport = hourlyPerformanceStats .map(([time, tokenObjects]) => { return tokenObjects.map(([token, values]) => { return { time: time, symbol: token, ...values } }) }) .flat() const title = `${ mangoAccount?.name || mangoAccount?.publicKey }-Performance-${new Date().toLocaleDateString()}` exportDataToCSV(dataToExport, title, HEADERS, t) } const calculateChartData = (chartToShow) => { const metrics = { 'account-value': (values) => { return ( values['spot_value'] + values['perp_value'] + values['open_orders_value'] ) }, 'account-pnl': (values) => { return ( values['spot_value'] + values['perp_value'] + values['open_orders_value'] - values['transfer_balance'] ) }, 'perp-pnl': (values) => { return ( values['perp_value'] + values['perp_spot_transfers_balance'] + values['mngo_rewards_value'] ) }, 'perp-pnl-ex-rewards': (values) => { return values['perp_value'] + values['perp_spot_transfers_balance'] }, // 'interest-cumulative': (values) => { // return ( // (values['deposit_interest_cumulative'] + // values['borrow_interest_cumulative']) * // values['price'] // ) // }, 'funding-cumulative': (values) => { return ( values['long_funding_cumulative'] + values['short_funding_cumulative'] ) }, 'mngo-rewards': (values) => { return values['mngo_rewards_value'] }, 'maker-volume': (values) => { return values['maker_cumulative'] }, 'taker-volume': (values) => { return values['taker_cumulative'] }, } const metric: (values: []) => void = metrics[chartToShow] let stats if (chartToShow !== 'maker-volume' && chartToShow !== 'taker-volume') { stats = hourlyPerformanceStats.map(([time, tokenObjects]) => { return { time: time, ...Object.fromEntries( tokenObjects.map(([token, values]) => [token, metric(values)]) ), All: tokenObjects .map(([_, values]) => metric(values)) .reduce((a, b) => a + b, 0), } }) } else { stats = volumeData.map(([time, tokenObjects]) => { return { time: time, ...Object.fromEntries( tokenObjects.map(([token, values]) => [token, metric(values)]) ), All: tokenObjects .map(([_, values]) => metric(values)) .reduce((a, b) => a + b, 0), } }) } // Normalise chart to start from 0 (except for account value) if (parseInt(performanceRange) !== 90 && chartToShow !== 'account-value') { const startValues = Object.assign({}, stats[0]) // Initialize symbol not present at the start to 0 uniqueSymbols .filter((e) => !(e in startValues)) .map((f) => (startValues[f] = 0)) for (let i = 0; i < stats.length; i++) { for (const key in stats[i]) { if (key !== 'time') { stats[i][key] = stats[i][key] - startValues[key] } } } } setChartData(stats) setChartToShow(chartToShow) } useEffect(() => { const fetchStats = async () => { setLoading(true) const promises = [ fetch( `https://mango-transaction-log.herokuapp.com/v3/stats/account-performance-per-token?mango-account=${mangoAccountPk}&start-date=${dayjs() .subtract(parseInt(performanceRange), 'day') .format('YYYY-MM-DD')}` ), fetch( `https://mango-transaction-log.herokuapp.com/v3/stats/volumes-by-mango-account?mango-account=${mangoAccountPk}&start-date=${dayjs() .subtract(parseInt(performanceRange), 'day') .format('YYYY-MM-DD')}` ), ] const data = await Promise.all(promises) const performanceData = await data[0].json() const volumeData = await data[1].json() let performanceEntries: any = Object.entries(performanceData) performanceEntries = performanceEntries .map(([key, value]) => [key, Object.entries(value)]) .reverse() let volumeEntries: any = Object.entries(volumeData) volumeEntries = volumeEntries.map(([key, value]) => [ key, Object.entries(value), ]) const uniqueSymbols = [ ...new Set( ([] as string[]).concat( ['All'], ...performanceEntries.map(([_, tokens]) => tokens.map(([token, _]) => token) ) ) ), ] const uniqueVolumeSymbols = [ ...new Set( ([] as string[]).concat( ['All'], ...volumeEntries.map(([_, tokens]) => tokens.map(([token, _]) => token) ) ) ), ] setUniqueSymbols(uniqueSymbols) setFilteredSymbols(uniqueSymbols) setVolumeSymbols(uniqueVolumeSymbols) setHourlyPerformanceStats(performanceEntries) setVolumeData(volumeEntries) setLoading(false) } fetchStats() }, [mangoAccountPk, performanceRange]) useEffect(() => { calculateChartData(chartToShow) }, [hourlyPerformanceStats, volumeData]) useEffect(() => { if ( ['perp-pnl', 'mngo-rewards', 'funding-cumulative'].includes(chartToShow) ) { setFilteredSymbols(uniqueSymbols.filter((s) => s !== 'USDC')) if (selectedSymbols.includes('USDC')) { setSelectedSymbols(selectedSymbols.filter((s) => s !== 'USDC')) } } else { if (selectAll) { setSelectedSymbols(uniqueSymbols) } setFilteredSymbols(uniqueSymbols) } }, [chartToShow, selectedSymbols, hourlyPerformanceStats]) const toggleOption = (v) => { selectedSymbols.includes(v) ? setSelectedSymbols(selectedSymbols.filter((item) => item !== v)) : setSelectedSymbols([...selectedSymbols, v]) } const handleSelectAll = () => { if (!selectAll) { setSelectedSymbols([...uniqueSymbols]) setSelectAll(true) } else { setSelectedSymbols([]) setSelectAll(false) } } const renderTooltip = (props) => { const { payload } = props return payload ? (

{dayjs(payload[0]?.payload.time).format('ddd D MMM YYYY')}

{payload.map((entry, index) => { return (

{entry.name}

{numberCompacter.format(entry.value)}

) })}
) : null } const numberCompacter = Intl.NumberFormat('en', { style: 'currency', currency: 'USD', notation: 'compact', maximumFractionDigits: 2, }) const renderSymbolIcon = (s) => { if (s === 'All') return if (chartToShow !== 'maker-volume' && chartToShow !== 'taker-volume') { const iconName = `${s.slice(0, 1)}${s.slice(1, 4).toLowerCase()}MonoIcon` const SymbolIcon = MonoIcons[iconName] || QuestionMarkCircleIcon return } else { const iconName = `${s.slice(0, 1)}${s.slice(1, -5).toLowerCase()}MonoIcon` const SymbolIcon = MonoIcons[iconName] || QuestionMarkCircleIcon return } } const symbols = useMemo(() => { if (chartToShow !== 'maker-volume' && chartToShow !== 'taker-volume') { return filteredSymbols } return volumeSymbols }, [chartToShow]) return ( <>

{t('account-performance')}

calculateChartData(cat)} values={DATA_CATEGORIES} names={DATA_CATEGORIES.map((val) => t(`account-performance:${val}`))} />
{mangoAccount ? ( <>

{`${t( `account-performance:${chartToShow}` )}`}

setPerformanceRange(p)} values={performanceRangePresetValues} names={performanceRangePresetLabels} />
{!loading ? ( chartData.length > 0 && selectedSymbols.length > 0 ? ( 0 ? false : true} dy={10} minTickGap={20} tick={{ fill: theme === 'Light' ? 'rgba(0,0,0,0.4)' : 'rgba(255,255,255,0.35)', fontSize: 10, }} tickLine={false} tickFormatter={(v) => dayjs(v).format('D MMM')} /> numberCompacter.format(v)} /> {selectedSymbols.map((v, i) => { const symbol = v.includes('/') || v.includes('-') ? v.slice(0, -5) : v return ( ) })} ) : selectedSymbols.length === 0 ? (

{t('account-performance:select-an-asset')}

) : (

{t('account-performance:no-data')}

) ) : loading ? (
) : null}

{t('assets')}

{t('select-all')}
{symbols.map((s) => { const symbol = s.includes('/') || s.includes('-') ? s.slice(0, -5) : s return ( ) })}
) : (
{t('connect-wallet')}
)} ) } export default AccountPerformance