From 634aa96716b799c6436560967d82b5674b72cff1 Mon Sep 17 00:00:00 2001 From: DaveVodrazka Date: Tue, 30 Jul 2024 11:40:25 +0200 Subject: [PATCH 1/6] feat: profit and loss --- .../PnL/NotionalVolumeLeaderboard.tsx | 101 ++++++++ src/components/PnL/PnL.tsx | 78 ++++++ src/components/PnL/getTrades.ts | 231 ++++++++++++++++++ src/components/PnL/index.ts | 4 + 4 files changed, 414 insertions(+) create mode 100644 src/components/PnL/NotionalVolumeLeaderboard.tsx create mode 100644 src/components/PnL/PnL.tsx create mode 100644 src/components/PnL/getTrades.ts create mode 100644 src/components/PnL/index.ts diff --git a/src/components/PnL/NotionalVolumeLeaderboard.tsx b/src/components/PnL/NotionalVolumeLeaderboard.tsx new file mode 100644 index 00000000..1dd0caec --- /dev/null +++ b/src/components/PnL/NotionalVolumeLeaderboard.tsx @@ -0,0 +1,101 @@ +import { useQuery } from "react-query"; +import { notionalVolumeQuery } from "./getTrades"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import { ClickableUser } from "../Points/Leaderboard"; + +import styles from "../Points/points.module.css"; +import tableStyles from "../../style/table.module.css"; +import { ReactNode } from "react"; + +const Item = ({ + volume, + address, + position, + sx, +}: { + volume: number; + address: string; + position: number; + sx?: any; +}) => { + const displayPosition = + position > 3 + ? position + "" + : position === 1 + ? "🥇" + : position === 2 + ? "🥈" + : "🥉"; + + return ( + + {displayPosition} + +
+ +
+
+ ${volume.toFixed(2)} +
+ ); +}; + +const Bold = ({ children }: { children: ReactNode }) => ( + {children} +); + +export const NotionalVolumeLeaderboard = () => { + const { isLoading, isError, data } = useQuery( + ["notional-volume-leaderboard"], + notionalVolumeQuery + ); + + if (isLoading) { + return
Loading...
; + } + + if (isError || !data) { + return
Something went wrong
; + } + + const sortedCallers = Object.entries(data) + .sort(([, a], [, b]) => b - a) // Sort by the number in descending order + .slice(0, 20); // Extract the top 20 callers + + return ( + + + + + + # + + + User + + + Notional Volume + + + + + {sortedCallers.map(([address, notionalVolumeUsd], i) => ( + + ))} + +
+
+ ); +}; diff --git a/src/components/PnL/PnL.tsx b/src/components/PnL/PnL.tsx new file mode 100644 index 00000000..712e231d --- /dev/null +++ b/src/components/PnL/PnL.tsx @@ -0,0 +1,78 @@ +import { useQuery } from "react-query"; +import { userPnLQuery } from "./getTrades"; +import { useAccount } from "../../hooks/useAccount"; +import { + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; + +export const ProfitAndLossWithAddress = ({ address }: { address: string }) => { + const { isLoading, isError, data } = useQuery( + [`trades-with-prices-${address}`, address], + userPnLQuery + ); + + if (isLoading) { + return
Loading...
; + } + + if (isError || !data) { + return
Something went wrong
; + } + + // Convert timestamp to a readable date format for the X axis + const formattedData = data.map((item) => ({ + usd: item.usd, + date: new Date(item.ts * 1000).toLocaleDateString(), + })); + + console.log(formattedData); + + return ( + + + + + + + + + + + ); +}; + +export const ProfitAndLoss = () => { + const account = useAccount(); + + if (!account) { + return
Need to connect wallet
; + } + + return ; +}; diff --git a/src/components/PnL/getTrades.ts b/src/components/PnL/getTrades.ts new file mode 100644 index 00000000..65853cc2 --- /dev/null +++ b/src/components/PnL/getTrades.ts @@ -0,0 +1,231 @@ +import { QueryFunctionContext } from "react-query"; +import { apiUrl } from "../../api"; + +export type TradeWithPrices = { + timestamp: number; + action: string; + caller: string; + capital_transfered: number; + capital_transfered_usd: number; + underlying_asset_price_usd: number; + tokens_minted: number; + premia: number; + premia_usd: number; + option_side: number; + option_type: number; + maturity: number; + strike_price: number; + pool_id: string; +}; + +export type PnL = { + ts: number; + usd: number; + change: number; + side: number; + pool: string; +}; + +export const getUserTrades = async ( + address: string +): Promise => { + const res = await fetch(apiUrl(`/trades?address=${address}`)).then((r) => + r.json() + ); + + if (res && res?.status === "success") { + return res.data as TradeWithPrices[]; + } + + throw Error("Failed getting trades with prices"); +}; + +export const getAllTrades = async (): Promise => { + const res = await fetch(apiUrl("trades")).then((r) => r.json()); + + if (res && res?.status === "success") { + return res.data as TradeWithPrices[]; + } + + throw Error("Failed getting trades with prices"); +}; + +export const userTradeQuery = async ({ + queryKey, +}: QueryFunctionContext<[string, string]>): Promise => { + const userAddress = queryKey[1]; + + return getUserTrades(userAddress); +}; + +const calculatePnL = (trades: TradeWithPrices[]): PnL[] => { + const sortedTrades = trades.sort((a, b) => a.timestamp - b.timestamp); + const fees = 0.03; + let balance = 0; + const pnl = []; + + const openShorts: { + [key: string]: number; + } = {}; + + for (const trade of sortedTrades) { + if (trade.action === "TradeOpen") { + // open long position -> pay premia + if (trade.option_side === 0) { + const change = -trade.premia_usd; + balance += change; + pnl.push({ + ts: trade.timestamp, + usd: balance, + change, + side: trade.option_side, + pool: trade.pool_id, + }); + } + // open short position -> keep track, change p&l when settled + if (trade.option_side === 1) { + const key = trade.pool_id + trade.strike_price + trade.maturity; + const tokens = trade.premia * (1 - fees) + trade.capital_transfered; + if (openShorts.hasOwnProperty(key)) { + openShorts[key] += tokens; + } else { + openShorts[key] = tokens; + } + } + } + if (trade.action === "TradeSettle") { + // settle long position - receive money (zero if OotM) + if (trade.option_side === 0) { + const change = trade.capital_transfered_usd; + if (change !== 0) { + balance += change; + pnl.push({ + ts: trade.timestamp, + usd: balance, + change, + side: trade.option_side, + pool: trade.pool_id, + }); + } + } + // check how much was locked and how much was returned + if (trade.option_side === 1) { + const key = trade.pool_id + trade.strike_price + trade.maturity; + const prev = openShorts[key]; + const diff = prev - trade.capital_transfered; + const change = diff * trade.underlying_asset_price_usd; + if (change !== 0) { + balance += change; + pnl.push({ + ts: trade.timestamp, + usd: balance, + change, + side: trade.option_side, + pool: trade.pool_id, + }); + } + } + } + if (trade.action === "TradeClose") { + // settle long position - receive money (zero if OotM) + if (trade.option_side === 0) { + const change = trade.capital_transfered_usd; + if (change !== 0) { + balance += change; + pnl.push({ + ts: trade.timestamp, + usd: balance, + change, + side: trade.option_side, + pool: trade.pool_id, + }); + } + } + // check how much was locked and how much was returned + if (trade.option_side === 1) { + const key = trade.pool_id + trade.strike_price + trade.maturity; + const prev = openShorts[key]; + const diff = prev - trade.capital_transfered; + const change = diff * trade.underlying_asset_price_usd; + if (change !== 0) { + balance += change; + pnl.push({ + ts: trade.timestamp, + usd: balance, + change, + side: trade.option_side, + pool: trade.pool_id, + }); + } + } + } + } + + return pnl; +}; + +export const userPnLQuery = async ({ + queryKey, +}: QueryFunctionContext<[string, string]>): Promise => { + const userAddress = queryKey[1]; + + const trades = await getUserTrades(userAddress); + + return calculatePnL(trades); +}; + +const calculateNotionalVolumeSingleUser = ( + trades: TradeWithPrices[] +): number => { + let total = 0; + const filtered = trades.filter((t) => t.action === "TradeOpen"); + filtered.forEach((trade) => { + if (trade.option_type === 1) { + total += + trade.underlying_asset_price_usd * + trade.strike_price * + trade.tokens_minted; + } else { + total += trade.underlying_asset_price_usd * trade.tokens_minted; + } + }); + + return total; +}; + +const calculateNotionalVolume = ( + trades: TradeWithPrices[] +): { [key: string]: number } => { + const userMap: { [key: string]: TradeWithPrices[] } = trades.reduce( + (acc, obj) => { + const key = obj.caller; + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(obj); + return acc; + }, + {} as { [key: string]: TradeWithPrices[] } + ); + + const result = Object.keys(userMap).reduce((acc, key) => { + acc[key] = calculateNotionalVolumeSingleUser(userMap[key]); + return acc; + }, {} as { [key: string]: number }); + + const sum = Object.values(result).reduce( + (accumulator, currentValue) => accumulator + currentValue, + 0 + ); + + console.log("TOTAL NOTIONAL VOLUME:", sum); + + return result; +}; + +export const notionalVolumeQuery = async ({ + queryKey, +}: QueryFunctionContext<[string]>): Promise<{ [key: string]: number }> => { + const allTrades = await getAllTrades(); + return calculateNotionalVolume(allTrades); +}; diff --git a/src/components/PnL/index.ts b/src/components/PnL/index.ts new file mode 100644 index 00000000..e8a61dad --- /dev/null +++ b/src/components/PnL/index.ts @@ -0,0 +1,4 @@ +import { NotionalVolumeLeaderboard } from "./NotionalVolumeLeaderboard"; +import { ProfitAndLoss } from "./PnL"; + +export { ProfitAndLoss, NotionalVolumeLeaderboard }; From 517310f34a1532b6b73366148bb591f97c451280 Mon Sep 17 00:00:00 2001 From: DaveVodrazka Date: Tue, 30 Jul 2024 13:25:14 +0200 Subject: [PATCH 2/6] fix: format pnl domain --- src/components/PnL/PnL.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/PnL/PnL.tsx b/src/components/PnL/PnL.tsx index 712e231d..831a8731 100644 --- a/src/components/PnL/PnL.tsx +++ b/src/components/PnL/PnL.tsx @@ -32,6 +32,18 @@ export const ProfitAndLossWithAddress = ({ address }: { address: string }) => { date: new Date(item.ts * 1000).toLocaleDateString(), })); + const formatDomain = ([min, max]: [number, number]): [number, number] => { + const minPadding = Math.max(Math.abs(min) * 0.1, 1); + const maxPadding = Math.max(Math.abs(max) * 0.1, 1); + + const finalDomain = [ + Math.round(min) - minPadding, + Math.round(max) + maxPadding, + ] as [number, number]; + + return finalDomain; + }; + console.log(formattedData); return ( @@ -47,7 +59,7 @@ export const ProfitAndLossWithAddress = ({ address }: { address: string }) => { > - + { /> Date: Tue, 30 Jul 2024 15:14:31 +0200 Subject: [PATCH 3/6] fix: better domain --- src/components/PnL/PnL.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/PnL/PnL.tsx b/src/components/PnL/PnL.tsx index 831a8731..ce223b6d 100644 --- a/src/components/PnL/PnL.tsx +++ b/src/components/PnL/PnL.tsx @@ -6,13 +6,14 @@ import { Legend, Line, LineChart, + ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; -export const ProfitAndLossWithAddress = ({ address }: { address: string }) => { +const ProfitAndLossWithAddress = ({ address }: { address: string }) => { const { isLoading, isError, data } = useQuery( [`trades-with-prices-${address}`, address], userPnLQuery @@ -33,12 +34,13 @@ export const ProfitAndLossWithAddress = ({ address }: { address: string }) => { })); const formatDomain = ([min, max]: [number, number]): [number, number] => { - const minPadding = Math.max(Math.abs(min) * 0.1, 1); - const maxPadding = Math.max(Math.abs(max) * 0.1, 1); + const minPadding = Math.ceil(Math.max(Math.abs(min) * 0.2, 1)); + const maxPadding = Math.ceil(Math.max(Math.abs(max) * 0.2, 1)); + const padding = Math.max(minPadding, maxPadding); const finalDomain = [ - Math.round(min) - minPadding, - Math.round(max) + maxPadding, + Math.min(Math.round(min) - padding, 0), + Math.round(max) + padding, ] as [number, number]; return finalDomain; @@ -74,6 +76,7 @@ export const ProfitAndLossWithAddress = ({ address }: { address: string }) => { stroke="#8884d8" activeDot={{ r: 8 }} /> + ); From 06e0f355f215d1bba5e9cc587a88ddeac35c5640 Mon Sep 17 00:00:00 2001 From: DaveVodrazka Date: Tue, 6 Aug 2024 17:39:33 +0200 Subject: [PATCH 4/6] feat: new leaderboard design --- public/index.html | 5 + src/App.tsx | 3 +- src/components/Leaderboard/Leaderboard.tsx | 87 +++++++++++++ src/components/Leaderboard/index.ts | 3 + .../Leaderboard/leaderboard.module.css | 103 +++++++++++++++ src/components/Leaderboard/wallet.svg | 5 + src/components/Leaderboard/whiteWallet.svg | 3 + .../PnL/NotionalVolumeLeaderboard.tsx | 119 ++++++------------ src/components/PnL/PnL.tsx | 2 - src/components/PnL/getTrades.ts | 70 ++++++++++- src/pages/battlecharts.tsx | 20 +++ src/pages/trade.tsx | 5 + 12 files changed, 335 insertions(+), 90 deletions(-) create mode 100644 src/components/Leaderboard/Leaderboard.tsx create mode 100644 src/components/Leaderboard/index.ts create mode 100644 src/components/Leaderboard/leaderboard.module.css create mode 100644 src/components/Leaderboard/wallet.svg create mode 100644 src/components/Leaderboard/whiteWallet.svg create mode 100644 src/pages/battlecharts.tsx diff --git a/public/index.html b/public/index.html index 039f679e..54fe89fc 100644 --- a/public/index.html +++ b/public/index.html @@ -13,6 +13,11 @@ + + + diff --git a/src/App.tsx b/src/App.tsx index b93451ca..85fbd4ad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,7 +16,6 @@ import { Controller } from "./Controller"; import APYInfoPage from "./pages/apyInfo"; import TradeDashboardPage from "./pages/dashboard"; import Governance from "./pages/governance"; -import PriceGuard from "./pages/priceGuard"; import NotFound from "./pages/notFound"; import Portfolio from "./pages/portfolio"; import Settings from "./pages/settings"; @@ -30,6 +29,7 @@ import { isCookieSet } from "./utils/cookies"; import "./style/base.css"; import LeaderboardPage from "./pages/leaderboard"; import StarknetRewards from "./pages/starknetRewards"; +import BattlechartsPage from "./pages/battlecharts"; const App = () => { const [check, rerender] = useState(false); @@ -70,6 +70,7 @@ const App = () => { } /> } /> } /> + } /> } /> diff --git a/src/components/Leaderboard/Leaderboard.tsx b/src/components/Leaderboard/Leaderboard.tsx new file mode 100644 index 00000000..7a19a484 --- /dev/null +++ b/src/components/Leaderboard/Leaderboard.tsx @@ -0,0 +1,87 @@ +import { addressElision } from "../../utils/utils"; +import { ReactComponent as BlackWalletIcon } from "./wallet.svg"; +import { ReactComponent as WalletIcon } from "./whiteWallet.svg"; + +import styles from "./leaderboard.module.css"; + +export type ItemProps = { + position: number; + address: string; + data: (string | JSX.Element)[]; + className?: string; +}; + +type Props = { + header: string[]; + items: ItemProps[]; + user?: ItemProps; +}; + +const LeaderboardItem = ({ position, address, data, className }: ItemProps) => { + return ( + + {position} + +
+ {position === 1 && } + {position === 2 && } + {position > 2 && } + {addressElision(address)} +
+ + {data.map((v) => ( + {v} + ))} + + ); +}; + +export const Leaderboard = ({ header, items, user }: Props) => { + const positionToClassName = (position: number) => { + if (position > 3 || position < 1) { + return; + } + if (position === 1) { + return styles.first; + } + if (position === 2) { + return styles.second; + } + if (position === 3) { + return styles.third; + } + // unreachable + return; + }; + + return ( + + + + {header.map((h, i) => ( + + ))} + + + + {user && ( + + )} + {items.map(({ position, address, data }, i) => ( + + ))} + +
{h}
+ ); +}; diff --git a/src/components/Leaderboard/index.ts b/src/components/Leaderboard/index.ts new file mode 100644 index 00000000..c8034242 --- /dev/null +++ b/src/components/Leaderboard/index.ts @@ -0,0 +1,3 @@ +import { Leaderboard } from "./Leaderboard"; + +export { Leaderboard }; diff --git a/src/components/Leaderboard/leaderboard.module.css b/src/components/Leaderboard/leaderboard.module.css new file mode 100644 index 00000000..d3f5341d --- /dev/null +++ b/src/components/Leaderboard/leaderboard.module.css @@ -0,0 +1,103 @@ +.leaderboard { + margin: 0 auto; + border-collapse: separate; + width: 100%; + font-family: "IBM Plex Sans", sans-serif; + font-weight: 600; + font-size: 15px; +} + +.leaderboard thead th { + text-align: left; + color: #969696; + text-transform: uppercase; +} + +.leaderboard thead tr { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + margin-bottom: 12px; +} + +.leaderboard tbody tr { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + margin-bottom: 12px; + border-radius: 2px; +} + +.wallet { + display: flex; + align-items: center; + gap: 5px; +} + +.user { + border: solid 1px var(--LIGHT-GREY); +} + +.first { + background: #FFC640; +} + +.first td { + color: black; +} + +.first td span { + color: black; +} + +.first td div { + color: black; +} + +.second { + background: #E1E1E1; + color: black; +} + +.second td { + color: black; +} + +.second td span { + color: black; +} + +.second td div { + color: black; +} + + +.third { + background: #381E0F; +} + +.leaderboard thead th { + width: 150px; +} + +.leaderboard thead th:nth-child(1) { + width: 50px; +} + +.leaderboard thead th:nth-child(2) { + width: 250px; +} + +.leaderboard td { + width: 150px; +} + +.leaderboard td:nth-child(1) { + width: 50px; +} + +.leaderboard td:nth-child(2) { + width: 250px; +} \ No newline at end of file diff --git a/src/components/Leaderboard/wallet.svg b/src/components/Leaderboard/wallet.svg new file mode 100644 index 00000000..103fc54e --- /dev/null +++ b/src/components/Leaderboard/wallet.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/components/Leaderboard/whiteWallet.svg b/src/components/Leaderboard/whiteWallet.svg new file mode 100644 index 00000000..00dcc22b --- /dev/null +++ b/src/components/Leaderboard/whiteWallet.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/PnL/NotionalVolumeLeaderboard.tsx b/src/components/PnL/NotionalVolumeLeaderboard.tsx index 1dd0caec..9437b1d4 100644 --- a/src/components/PnL/NotionalVolumeLeaderboard.tsx +++ b/src/components/PnL/NotionalVolumeLeaderboard.tsx @@ -1,60 +1,13 @@ import { useQuery } from "react-query"; -import { notionalVolumeQuery } from "./getTrades"; -import { - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, -} from "@mui/material"; -import { ClickableUser } from "../Points/Leaderboard"; - -import styles from "../Points/points.module.css"; -import tableStyles from "../../style/table.module.css"; -import { ReactNode } from "react"; - -const Item = ({ - volume, - address, - position, - sx, -}: { - volume: number; - address: string; - position: number; - sx?: any; -}) => { - const displayPosition = - position > 3 - ? position + "" - : position === 1 - ? "🥇" - : position === 2 - ? "🥈" - : "🥉"; - - return ( - - {displayPosition} - -
- -
-
- ${volume.toFixed(2)} -
- ); -}; - -const Bold = ({ children }: { children: ReactNode }) => ( - {children} -); +import { tradeLeaderboardDataQuery } from "./getTrades"; +import { Leaderboard } from "../Leaderboard"; +import { useAccount } from "../../hooks/useAccount"; export const NotionalVolumeLeaderboard = () => { + const account = useAccount(); const { isLoading, isError, data } = useQuery( - ["notional-volume-leaderboard"], - notionalVolumeQuery + ["notional-volume-leaderboard", account?.address], + tradeLeaderboardDataQuery ); if (isLoading) { @@ -65,37 +18,35 @@ export const NotionalVolumeLeaderboard = () => { return
Something went wrong
; } - const sortedCallers = Object.entries(data) - .sort(([, a], [, b]) => b - a) // Sort by the number in descending order - .slice(0, 20); // Extract the top 20 callers + const [leaderboardUsers, currentUser] = data; - return ( - - - - - - # - - - User - - - Notional Volume - - - - - {sortedCallers.map(([address, notionalVolumeUsd], i) => ( - - ))} - -
-
+ const header = ["Rank", "Trader", "Profit/Loss", "Volume"]; + + const parseData = (pnl: number, vol: number): [JSX.Element, JSX.Element] => { + const isPnlNegative = pnl < 0; + const PnlElem = ( + + {isPnlNegative && "-"}${Math.abs(pnl).toFixed(2)} + + ); + const VolElem = ${vol.toFixed(2)}; + + return [PnlElem, VolElem]; + }; + + const items = leaderboardUsers.map( + ({ address, notionalVolume, pnl, position }) => ({ + position, + address, + data: parseData(pnl, notionalVolume), + }) ); + + const user = currentUser && { + position: currentUser.position, + address: account!.address, + data: parseData(currentUser.pnl, currentUser.notionalVolume), + }; + + return ; }; diff --git a/src/components/PnL/PnL.tsx b/src/components/PnL/PnL.tsx index ce223b6d..58f7d76e 100644 --- a/src/components/PnL/PnL.tsx +++ b/src/components/PnL/PnL.tsx @@ -46,8 +46,6 @@ const ProfitAndLossWithAddress = ({ address }: { address: string }) => { return finalDomain; }; - console.log(formattedData); - return ( ): Promise<{ [key: string]: number }> => { +type TradeLeaderboardData = { + address: string; + notionalVolume: number; + pnl: number; + position: number; +}; + +const calculateLeaderboardData = ( + trades: TradeWithPrices[], + address?: string +): [TradeLeaderboardData[], TradeLeaderboardData | undefined] => { + const userMap: { [key: string]: TradeWithPrices[] } = trades.reduce( + (acc, obj) => { + const key = obj.caller; + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(obj); + return acc; + }, + {} as { [key: string]: TradeWithPrices[] } + ); + + const result = Object.keys(userMap).reduce((acc, key) => { + acc[key] = calculateNotionalVolumeSingleUser(userMap[key]); + return acc; + }, {} as { [key: string]: number }); + + const sortedCallers = Object.entries(result).sort(([, a], [, b]) => b - a); + + const topUsers = sortedCallers.slice(0, 20); // Extract the top 20 callers + + const tradeLeaderboardData = topUsers.map(([address, notionalVolume], i) => ({ + address, + notionalVolume, + position: i + 1, + pnl: calculatePnL(userMap[address]).at(-1)?.usd || 0, + })); + + if (!address) { + return [tradeLeaderboardData, undefined]; + } + + const index = sortedCallers.findIndex(([a, _]) => a === address); + + const user = { + address, + notionalVolume: calculateNotionalVolumeSingleUser(userMap[address]), + position: index + 1, + pnl: calculatePnL(userMap[address]).at(-1)?.usd || 0, + }; + + return [tradeLeaderboardData, user]; +}; + +export const notionalVolumeQuery = async (): Promise<{ + [key: string]: number; +}> => { const allTrades = await getAllTrades(); return calculateNotionalVolume(allTrades); }; + +export const tradeLeaderboardDataQuery = async ({ + queryKey, +}: QueryFunctionContext<[string, string | undefined]>): Promise< + [TradeLeaderboardData[], TradeLeaderboardData | undefined] +> => { + const allTrades = await getAllTrades(); + return calculateLeaderboardData(allTrades, queryKey[1]); +}; diff --git a/src/pages/battlecharts.tsx b/src/pages/battlecharts.tsx new file mode 100644 index 00000000..6da6adf8 --- /dev/null +++ b/src/pages/battlecharts.tsx @@ -0,0 +1,20 @@ +import { Layout } from "../components/Layout"; +import { CrmBanner } from "../components/Banner"; +import { Helmet } from "react-helmet"; +import { NotionalVolumeLeaderboard } from "../components/PnL"; + +const BattlechartsPage = () => { + return ( + + + Battlecharts | Carmine Options AMM + + + +

Trading Leaderboard

+ +
+ ); +}; + +export default BattlechartsPage; diff --git a/src/pages/trade.tsx b/src/pages/trade.tsx index db33be9e..964c4aa3 100644 --- a/src/pages/trade.tsx +++ b/src/pages/trade.tsx @@ -9,6 +9,7 @@ import buttonStyles from "../style/button.module.css"; import style from "./trade.module.css"; import { AvnuWidget } from "../components/AvnuWidget"; import { CrmBanner } from "../components/Banner"; +import { ProfitAndLoss } from "../components/PnL"; enum Variant { Options, @@ -48,6 +49,10 @@ const TradePage = () => { {variant === Variant.Swap && } {variant === Variant.Options && } + +
+ +
); }; From 6f1f7c1513a86b0f73fb9373e278685bb5b9c288 Mon Sep 17 00:00:00 2001 From: DaveVodrazka Date: Wed, 7 Aug 2024 10:35:13 +0200 Subject: [PATCH 5/6] feat: add battlecharts header link --- src/components/Header/Header.tsx | 18 +++++++++++++++++- src/components/Header/header.module.css | 18 +++++++++++++++--- src/components/Leaderboard/Leaderboard.tsx | 1 + .../Leaderboard/leaderboard.module.css | 12 ++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index d3e02553..17dd95d9 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -37,6 +37,10 @@ const navLinks = [ title: "Rewards", link: "/rewards", }, + { + title: "Battlecharts", + link: "/battlecharts", + }, ] as NavLinkProps[]; const RewardsTitle = () => ( @@ -45,6 +49,12 @@ const RewardsTitle = () => ( ); +const NewTitle = ({ title }: { title: string }) => ( +
+
NEW
{title} +
+); + const navLink = ({ title, link }: NavLinkProps, i: number): ReactNode => ( { @@ -61,7 +71,13 @@ const navLink = ({ title, link }: NavLinkProps, i: number): ReactNode => ( to={link} key={i} > - {title === "Rewards" ? : title} + {title === "Rewards" ? ( + + ) : title === "Battlecharts" ? ( + + ) : ( + title + )} ); diff --git a/src/components/Header/header.module.css b/src/components/Header/header.module.css index 235adbee..cf5ac294 100644 --- a/src/components/Header/header.module.css +++ b/src/components/Header/header.module.css @@ -7,14 +7,19 @@ .navlink { color: white; - margin: 1em 1.5em; + margin: 1em 1.2em; text-decoration: none; + font-size: 18px; } .active { color: rgba(255, 255, 255, 0.6); } +.active div { + color: rgba(255, 255, 255, 0.6); +} + .navlinkcontainer { display: flex; flex-flow: row; @@ -27,13 +32,20 @@ .logo { display: flex; margin-right: auto; - padding-right: 4rem; } .rewardsheader { display: flex; align-items: center; - gap: 15px; + gap: 10px; +} + +.badge { + background: #FAB000; + color: black; + font-size: 12px; + padding: 0 5px; + border-radius: 3px; } @media (max-width: 720px) { diff --git a/src/components/Leaderboard/Leaderboard.tsx b/src/components/Leaderboard/Leaderboard.tsx index 7a19a484..cb7bb057 100644 --- a/src/components/Leaderboard/Leaderboard.tsx +++ b/src/components/Leaderboard/Leaderboard.tsx @@ -32,6 +32,7 @@ const LeaderboardItem = ({ position, address, data, className }: ItemProps) => { {data.map((v) => ( {v} ))} + {className === styles.user &&
YOU
} ); }; diff --git a/src/components/Leaderboard/leaderboard.module.css b/src/components/Leaderboard/leaderboard.module.css index d3f5341d..b2a15e5a 100644 --- a/src/components/Leaderboard/leaderboard.module.css +++ b/src/components/Leaderboard/leaderboard.module.css @@ -23,6 +23,7 @@ .leaderboard tbody tr { display: flex; + position: relative; align-items: center; justify-content: space-between; padding: 12px; @@ -40,6 +41,17 @@ border: solid 1px var(--LIGHT-GREY); } +.badge { + background: #FAB000; + color: black; + font-size: 12px; + padding: 0 5px; + border-radius: 3px; + position: absolute; + top: -10px; + left: 9px; +} + .first { background: #FFC640; } From 9fbae03ecfb9b330dd2b82e0a279930116c5f989 Mon Sep 17 00:00:00 2001 From: DaveVodrazka Date: Wed, 7 Aug 2024 10:48:54 +0200 Subject: [PATCH 6/6] fix: remove pnl graph --- src/pages/trade.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pages/trade.tsx b/src/pages/trade.tsx index 964c4aa3..db33be9e 100644 --- a/src/pages/trade.tsx +++ b/src/pages/trade.tsx @@ -9,7 +9,6 @@ import buttonStyles from "../style/button.module.css"; import style from "./trade.module.css"; import { AvnuWidget } from "../components/AvnuWidget"; import { CrmBanner } from "../components/Banner"; -import { ProfitAndLoss } from "../components/PnL"; enum Variant { Options, @@ -49,10 +48,6 @@ const TradePage = () => { {variant === Variant.Swap && } {variant === Variant.Options && } - -
- -
); };