UNPKG

@swapper-finance/sdk

Version:
171 lines (155 loc) 5.49 kB
import { CASH_TOKEN_ADDRESS_BY_CHAIN } from "@src/config"; import { useHistory, useSwapContext } from "@src/contexts"; import { useMemo } from "react"; type TokenStats = { tokenAddress: string; chainId: string; netAmount: number; avgCost: number; currentPrice: number; currentValue: number; realizedPnL: number; unrealizedPnL: number; allTimePnL: number; allTimePnLPercent: number; }; type TradeType = "sell" | "buy"; export const usePnL = () => { const { tokensPrices, allTokens, tokensBalances } = useSwapContext(); const { history } = useHistory(); const sortedHistory = useMemo( () => [...history].sort( (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), ), [history], ); const rawCash = useMemo( () => allTokens.filter((t) => { const cashAddr = CASH_TOKEN_ADDRESS_BY_CHAIN[t.chainId].toLowerCase(); const tAddr = t.address.toLowerCase(); const balObj = tokensBalances[t.chainId]?.[tAddr]; return cashAddr && tAddr === cashAddr && !!balObj && balObj.wei !== "0"; }), [allTokens, tokensBalances], ); const cashWithValue = useMemo(() => { return rawCash .map((t) => { const tAddr = t.address.toLowerCase(); const balances = tokensBalances[t.chainId]?.[tAddr]; if (!balances) return null; const balance = parseFloat(balances.humanReadable); const price = 1; return { ...t, price, value: balance * price, balances, }; }) .filter((item): item is NonNullable<typeof item> => item !== null) .sort((a, b) => b.value - a.value); }, [rawCash, tokensBalances]); const totalCashValue = useMemo( () => cashWithValue.reduce((sum, t) => sum + t.value, 0), [cashWithValue], ); const pnlData = useMemo(() => { if (!sortedHistory || !Array.isArray(sortedHistory)) return null; if (!tokensPrices) return null; const portfolio = new Map< string, { trades: { amount: number; usd: number; type: TradeType }[]; } >(); for (const tx of sortedHistory) { const { tokenAddress, action, priceUsd, chainId, tokenAmount } = tx; const tokenDecimals = allTokens.find( (t) => t.address === tokenAddress, )?.decimals; if (!tokenDecimals || !chainId) continue; if (!["Bought", "Sold", "Transferred"].includes(action)) continue; if (!tokenAmount || !priceUsd) continue; const usd = parseFloat(priceUsd); const amount = parseFloat(tokenAmount) / 10 ** tokenDecimals; const key = `${chainId}_${tokenAddress}`; if (!portfolio.has(key)) { portfolio.set(key, { trades: [] }); } const entry = portfolio.get(key)!; if (action === "Bought") { entry.trades.push({ amount, usd, type: "buy" }); } else if (action === "Sold" || action === "Transferred") { entry.trades.push({ amount, usd, type: "sell" }); } } const tokenPnLs: TokenStats[] = []; let totalRealizedPnL = 0; let totalUnrealizedPnL = 0; let totalInvested = 0; for (const [key, { trades }] of portfolio) { const [chainId, tokenAddress] = key.split("_"); if (!chainId || !tokenAddress) continue; const currentPrice = tokensPrices?.[chainId]?.[tokenAddress]; if (!currentPrice) continue; let totalTokenAmountHolding = 0; let totalBoughtUsd = 0; let averageCost = 0; let realizedPnL = 0; for (const { amount: tokenAmount, usd: usdValue, type } of trades) { if (type === "buy") { averageCost = (averageCost * totalTokenAmountHolding + usdValue) / (totalTokenAmountHolding + tokenAmount); totalTokenAmountHolding += tokenAmount; totalBoughtUsd += usdValue; } else if (type === "sell") { const soldTokenAmount = Math.min( tokenAmount, totalTokenAmountHolding, ); let soldUsdValue = usdValue; if (soldTokenAmount < tokenAmount) { soldUsdValue *= soldTokenAmount / tokenAmount; } const costBasis = soldTokenAmount * averageCost; realizedPnL += soldUsdValue - costBasis; totalTokenAmountHolding -= soldTokenAmount; } } const remainingTokenAmount = totalTokenAmountHolding; const unrealizedPnL = (currentPrice - averageCost) * remainingTokenAmount; const allTimePnL = realizedPnL + unrealizedPnL; const allTimePnLPercent = totalBoughtUsd > 0 ? (allTimePnL / totalBoughtUsd) * 100 : 0; totalRealizedPnL += realizedPnL; totalUnrealizedPnL += unrealizedPnL; totalInvested += totalBoughtUsd; tokenPnLs.push({ tokenAddress, chainId, netAmount: remainingTokenAmount, avgCost: averageCost, currentPrice, currentValue: remainingTokenAmount * currentPrice, realizedPnL, unrealizedPnL, allTimePnL, allTimePnLPercent, }); } const total = totalInvested + totalCashValue; return { tokenPnLs, totalRealizedPnL, totalUnrealizedPnL, totalAllTimePnL: totalRealizedPnL + totalUnrealizedPnL, totalAllTimePnLPercent: total > 0 ? ((totalRealizedPnL + totalUnrealizedPnL) / total) * 100 : 0, }; }, [sortedHistory, tokensPrices, allTokens, totalCashValue]); return pnlData; };