@swapper-finance/sdk
Version:
JavaScript SDK form Swapper
171 lines (155 loc) • 5.49 kB
text/typescript
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;
};