Pnl leaderboard (#43)

* leaderboard layout

* hook up pnl endpoint

* api limits, offset and start_date

* pnl history charts

* support account names

* ui feedback updates

* play with leaderboard 🙈

* fix chart bug

Co-authored-by: Maximilian Schneider <mail@maximilianschneider.net>
This commit is contained in:
saml33 2021-07-19 19:51:40 +10:00 committed by GitHub
parent cae7d92a16
commit 522425d90b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 425 additions and 4 deletions

8
@types/index.d.ts vendored
View File

@ -1,4 +1,12 @@
import 'dayjs'
declare module '*.svg' {
const content: any
export default content
}
declare module 'dayjs' {
interface Dayjs {
utc()
}
}

View File

@ -0,0 +1,242 @@
import { useEffect, useState } from 'react'
import styled from '@emotion/styled'
import dayjs from 'dayjs'
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'
import { AreaChart, Area, ReferenceLine, XAxis, YAxis, Tooltip } from 'recharts'
import { ExternalLinkIcon } from '@heroicons/react/outline'
import { usdFormatter } from '../utils'
import { AwardIcon, TrophyIcon } from './icons'
import useMangoStore from '../stores/useMangoStore'
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const StyledTooltipWrapper = styled.div`
min-width: 180px;
`
const LeaderboardTable = () => {
const [pnlHistory, setPnlHistory] = useState([])
const [loading, setLoading] = useState(false)
const pnlLeaderboard = useMangoStore((s) => s.pnlLeaderboard)
/* API Returns:
* [ { cumulative_pnl: -3.687498
date: "2021-06-10"
margin_account: "J8XtwLVyZjeH1PG1Nnk9cWbLn3zEemS1rCbn4x6AjtXM"
name: ""
owner: "APLKzSqJQw79q4U4ipBWnLdqkVzijSPNpDCNKwL8mW3B"
}, ... ]
*/
useEffect(() => {
const getPnlHistory = async () => {
setLoading(true)
const start = dayjs().utc().subtract(31, 'day').format('YYYY-MM-DD')
console.log(start)
const results = await Promise.all(
pnlLeaderboard.slice(pnlHistory.length).map(async (acc) => {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/stats/pnl_history/${acc.margin_account}?start_date=${start}`
)
const parsedResponse = await response.json()
return parsedResponse ? parsedResponse.reverse() : []
})
)
setPnlHistory(pnlHistory.concat(results))
setLoading(false)
}
getPnlHistory()
}, [pnlLeaderboard])
const formatPnlHistoryData = (data) => {
const start = new Date(
dayjs().utc().hour(0).minute(0).subtract(31, 'day')
).getTime()
return data.filter((d) => new Date(d.date).getTime() > start)
}
const tooltipContent = (tooltipProps) => {
if (tooltipProps.payload.length > 0) {
return (
<StyledTooltipWrapper className="bg-th-bkg-1 flex p-2 rounded">
<div>
<div className="text-th-fgd-3 text-xs">Date</div>
<div className="font-bold text-th-fgd-1 text-xs">
{tooltipProps.payload[0].payload.date}
</div>
</div>
<div className="pl-3">
<div className="text-th-fgd-3 text-xs">PNL</div>
<div className="font-bold text-th-fgd-1 text-xs">
{usdFormatter.format(
tooltipProps.payload[0].payload.cumulative_pnl
)}
</div>
</div>
</StyledTooltipWrapper>
)
}
return null
}
return (
<div className={`flex flex-col py-4`}>
<div className={`-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8`}>
<div className={`align-middle inline-block min-w-full sm:px-6 lg:px-8`}>
{pnlLeaderboard.length > 0 ? (
<div className={`shadow overflow-hidden border-b border-th-bkg-2`}>
<Table className={`min-w-full divide-y divide-th-bkg-2`}>
<Thead>
<Tr className="text-th-fgd-3 text-xs">
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Rank
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Account
</Th>
<Th
scope="col"
className={`px-6 py-3 text-right font-normal`}
>
PNL
</Th>
<Th
scope="col"
className={`px-6 py-3 text-right font-normal`}
>
PNL / Time
</Th>
<Th
scope="col"
className={`px-6 py-3 text-right font-normal`}
>
<div className="flex items-center justify-start md:justify-end">
<span>View on Step</span>
<img
alt=""
width="20"
height="20"
src="/assets/icons/step.png"
className={`ml-1`}
/>
</div>
</Th>
</Tr>
</Thead>
<Tbody>
{pnlLeaderboard.map((acc, index) => (
<Tr
key={acc.margin_account}
className={`border-b border-th-bkg-3
${index % 2 === 0 ? `bg-th-bkg-3` : `bg-th-bkg-2`}
`}
>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1 w-8`}
>
<div className="flex items-center">
{acc.rank}
{acc.rank === 1 ? (
<TrophyIcon className="h-5 w-5 ml-1.5 text-th-primary" />
) : null}
{acc.rank === 2 || acc.rank === 3 ? (
<AwardIcon className="h-5 w-5 ml-1.5 text-th-primary-dark" />
) : null}
</div>
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-left text-sm text-th-fgd-1 md:w-1/3`}
>
{acc.name
? acc.name
: `${acc.margin_account.slice(
0,
5
)}...${acc.margin_account.slice(-5)}`}
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<div className="flex md:justify-end">
{usdFormatter.format(acc.pnl)}
</div>
</Td>
<Td
className={`flex justify-end px-6 py-3 whitespace-nowrap`}
>
{loading && !pnlHistory[index] ? (
<div className="animate-pulse bg-th-fgd-4 h-14 opacity-10 rounded-md w-44" />
) : (
<AreaChart
width={176}
height={56}
data={
pnlHistory[index]
? formatPnlHistoryData(pnlHistory[index])
: null
}
>
<ReferenceLine
y={0}
stroke="#FF9C24"
strokeDasharray="3 3"
strokeOpacity={0.6}
/>
<Area
isAnimationActive={false}
type="monotone"
dataKey="cumulative_pnl"
stroke="#FF9C24"
fill="#FF9C24"
fillOpacity={0.1}
/>
<XAxis dataKey="date" hide />
<YAxis dataKey="cumulative_pnl" hide />
<Tooltip
content={tooltipContent}
position={{ x: 0, y: -50 }}
/>
</AreaChart>
)}
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<a
className="default-transition flex items-center md:justify-end text-th-fgd-2"
href={`https://app.step.finance/#/watch/${acc.owner}`}
target="_blank"
rel="noopener noreferrer"
>
<span>View</span>
<ExternalLinkIcon className={`h-4 w-4 ml-1.5`} />
</a>
</Td>
</Tr>
))}
</Tbody>
</Table>
</div>
) : (
<div className="pt-2">
<div className="animate-pulse bg-th-bkg-3 h-10 mb-2 rounded-md w-full" />
<div className="animate-pulse bg-th-bkg-3 h-10 mb-2 rounded-md w-full" />
<div className="animate-pulse bg-th-bkg-3 h-10 mb-2 rounded-md w-full" />
<div className="animate-pulse bg-th-bkg-3 h-10 mb-2 rounded-md w-full" />
<div className="animate-pulse bg-th-bkg-3 h-10 mb-2 rounded-md w-full" />
</div>
)}
</div>
</div>
</div>
)
}
export default LeaderboardTable

View File

@ -31,6 +31,7 @@ const TopBar = () => {
<MenuItem href="/account">Account</MenuItem>
<MenuItem href="/borrow">Borrow</MenuItem>
<MenuItem href="/alerts">Alerts</MenuItem>
<MenuItem href="/leaderboard">Leaderboard</MenuItem>
<MenuItem href="/stats">Stats</MenuItem>
<MenuItem href="https://docs.mango.markets/">Learn</MenuItem>
</div>

View File

@ -147,7 +147,7 @@ export default function AccountAssets() {
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Available
Deposits
</Th>
<Th
scope="col"

View File

@ -141,3 +141,32 @@ export const ProfileIcon = ({ className }) => {
</svg>
)
}
export const AwardIcon = ({ className }) => {
return (
<svg
className={`${className}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="12" cy="8" r="7"></circle>
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline>
</svg>
)
}
export const TrophyIcon = ({ className }) => {
return (
<svg
className={`${className}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 38 34"
fill="currentColor"
>
<path d="M36.0082 3.8971H30.7152V1.9751C30.7152 1.1461 30.0432 0.475098 29.2152 0.475098H8.78519C7.95619 0.475098 7.28519 1.1461 7.28519 1.9751V3.8971L1.99219 3.8981C1.16319 3.8981 0.492188 4.5701 0.492188 5.3981C0.492188 9.4881 3.47119 12.8871 7.37219 13.5611C7.87919 17.8821 10.7462 21.4901 14.6442 23.0591V25.6651H14.1752C13.7802 25.6651 13.4012 25.8211 13.1202 26.1001C11.5172 27.6871 10.6362 29.7911 10.6362 32.0241C10.6362 32.8521 11.3082 33.5241 12.1362 33.5241H25.8622C26.6902 33.5241 27.3622 32.8521 27.3622 32.0241C27.3622 29.7921 26.4802 27.6881 24.8792 26.1001C24.5992 25.8221 24.2192 25.6651 23.8232 25.6651H23.3542V23.0601C27.2532 21.4901 30.1212 17.8831 30.6272 13.5621C34.5272 12.8881 37.5072 9.4891 37.5072 5.3981C37.5082 4.5681 36.8362 3.8971 36.0082 3.8971ZM7.28519 10.4751C5.57019 9.9671 4.21519 8.6131 3.70819 6.8981H7.28519V10.4751ZM24.1462 30.5241H13.8532C14.0512 29.8511 14.3852 29.2211 14.8382 28.6661H23.1622C23.6152 29.2221 23.9472 29.8521 24.1462 30.5241ZM17.6452 25.6661V23.8171C18.0902 23.8691 18.5412 23.9041 18.9992 23.9041C19.4592 23.9041 19.9092 23.8691 20.3542 23.8171V25.6661H17.6452ZM27.7152 12.1911C27.7152 16.9961 23.8052 20.9041 18.9992 20.9041C14.1932 20.9041 10.2852 16.9961 10.2852 12.1911V3.4751H27.7152V12.1911ZM30.7152 10.4751V6.8981L34.2912 6.8991C33.7842 8.6131 32.4302 9.9671 30.7152 10.4751Z" />
</svg>
)
}

47
pages/leaderboard.tsx Normal file
View File

@ -0,0 +1,47 @@
import { useEffect, useState } from 'react'
import useMangoStore from '../stores/useMangoStore'
import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar'
import LeaderboardTable from '../components/LeaderboardTable'
import { LinkButton } from '../components/Button'
export default function Leaderboard() {
const [offsetResults, setOffsetResults] = useState(0)
const actions = useMangoStore((s) => s.actions)
const pnlLeaderboard = useMangoStore((s) => s.pnlLeaderboard)
useEffect(() => {
actions.fetchPnlLeaderboard(offsetResults, 29)
}, [])
const handleShowMore = async () => {
const offset = offsetResults + 25
await actions.fetchPnlLeaderboard(offset, 29)
setOffsetResults(offset)
}
return (
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<TopBar />
<PageBodyContainer>
<div className="pt-8 pb-3 sm:pb-6 md:pt-10">
<h1 className={`font-semibold text-th-fgd-1 text-2xl`}>
Leaderboard
</h1>
</div>
<div className="p-6 rounded-lg bg-th-bkg-2">
<p className="mb-0">Top 100 accounts by PNL over the last 30 days</p>
<LeaderboardTable />
{pnlLeaderboard.length < 100 ? (
<LinkButton
className="flex h-10 items-center justify-center mt-1 text-th-primary w-full"
onClick={() => handleShowMore()}
>
Show More
</LinkButton>
) : null}
</div>
</PageBodyContainer>
</div>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -10,11 +10,15 @@ import {
} from '@blockworks-foundation/mango-client'
import { SRM_DECIMALS } from '@project-serum/serum/lib/token-instructions'
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'
import dayjs from 'dayjs'
import { EndpointInfo, WalletAdapter } from '../@types/types'
import { getWalletTokenInfo } from '../utils/tokens'
import { isDefined } from '../utils/index'
import { notify } from '../utils/notifications'
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
export const ENDPOINTS: EndpointInfo[] = [
{
name: 'mainnet-beta',
@ -57,6 +61,14 @@ interface AccountInfoList {
[key: string]: AccountInfo<Buffer>
}
interface AccountPnl {
margin_account: string
name: string
owner: string
pnl: number
rank: number
}
interface MangoStore extends State {
notifications: Array<{
type: string
@ -119,9 +131,12 @@ interface MangoStore extends State {
liquidationHistory: any[]
withdrawalHistory: any[]
tradeHistory: any[]
pnlHistory: any[]
pnlLeaderboard: any[]
accountPnl: AccountPnl
set: (x: any) => void
actions: {
[key: string]: () => void
[key: string]: (...args: any[]) => void
}
}
@ -177,6 +192,9 @@ const useMangoStore = create<MangoStore>((set, get) => ({
liquidationHistory: [],
withdrawalHistory: [],
tradeHistory: [],
pnlHistory: [],
pnlLeaderboard: [],
accountPnl: null,
set: (fn) => set(produce(fn)),
actions: {
async fetchWalletBalances() {
@ -428,6 +446,82 @@ const useMangoStore = create<MangoStore>((set, get) => ({
state.withdrawalHistory = results
})
},
async fetchPnlHistory(marginAccount = null) {
const selectedMarginAccount =
marginAccount ||
get().selectedMarginAccount.current.publicKey.toString()
const set = get().set
if (!selectedMarginAccount) return
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/stats/pnl_history/${selectedMarginAccount}`
)
const parsedResponse = await response.json()
const results = parsedResponse.length ? parsedResponse.reverse() : []
set((state) => {
state.pnlHistory = results
})
},
async fetchPnlLeaderboard(offset = 0, start?: number) {
const baseUrl =
'https://mango-transaction-log.herokuapp.com/stats/pnl_leaderboard'
const startAt = start
? dayjs().utc().subtract(start, 'day').format('YYYY-MM-DD')
: null
const url = startAt
? `${baseUrl}?start_date=${startAt}&limit=25&offset=${offset}`
: `${baseUrl}?limit=25&offset=${offset}`
const response = await fetch(url)
const parsedResponse = await response.json()
const results = parsedResponse ? parsedResponse : []
const currentLeaderboard = get().pnlLeaderboard
if (currentLeaderboard.length > 0 && offset > 0) {
const updatedLeaderboard = currentLeaderboard.concat(results)
set((state) => {
state.pnlLeaderboard = updatedLeaderboard
})
} else {
set((state) => {
state.pnlLeaderboard = results
})
}
},
async fetchPnlByAccount(marginAccount = null, start?: number) {
const selectedMarginAccount =
marginAccount || get().selectedMarginAccount.current
const set = get().set
if (!selectedMarginAccount) return
const startAt = start
? new Date(Date.now() - start * 24 * 60 * 60 * 1000).toLocaleDateString(
'en-ZA'
)
: null
const baseUrl =
'https://mango-transaction-log.herokuapp.com/stats/pnl_leaderboard_rank'
const url = startAt
? `${baseUrl}/${selectedMarginAccount.publicKey.toString()}?start_date=${startAt}`
: `${baseUrl}/${selectedMarginAccount.publicKey.toString()}`
const response = await fetch(url)
const parsedResponse =
response.status === 200 ? await response.json() : null
const results = parsedResponse ? parsedResponse : null
set((state) => {
state.accountPnl = results
})
},
},
}))

View File

@ -2515,9 +2515,9 @@ boolbase@^1.0.0, boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
"borsh@git+https://github.com/defactojob/borsh-js.git#field-mapper":
"borsh@https://github.com/defactojob/borsh-js#field-mapper":
version "0.3.1"
resolved "git+https://github.com/defactojob/borsh-js.git#33a0d24af281112c0a48efb3fa503f3212443de9"
resolved "https://github.com/defactojob/borsh-js#33a0d24af281112c0a48efb3fa503f3212443de9"
dependencies:
"@types/bn.js" "^4.11.5"
bn.js "^5.0.0"