Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/app/api/fee-compare/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ const EVM_WALLET_VENUES = new Set(["hyperliquid", "gains", "gmx-v2"]);
// Rate caches
// ──────────────────────────────────────────────────────────────────────

// Gains v6 borrowingRatePerSecondP and lastFundingRatePerSecondP use 1e18 precision
const GAINS_CARRY_PRECISION = 1e18;
// Gains v6 borrowingRatePerSecondP uses 1e18; lastFundingRatePerSecondP uses 1e21
const GAINS_BORROW_PRECISION = 1e18;
const GAINS_FUNDING_PRECISION = 1e21;
// Fallback when API doesn't expose per-pair data (~0.04%/day expressed per-second)
const GAINS_BORROW_DEFAULT_PER_SEC = 0.0004 / 86400;
// USDC collateral index on Arbitrum (same as collateralIndex filter in fetchGainsTrades)
const GAINS_USDC_COLLATERAL_IDX = 3;
// USDC is array[2] in vars.collaterals (0-indexed); its collateralIndex field = 3 (1-indexed).
// The collateralIndex===3 filter in fetchGainsTrades refers to the field value, not the array position.
const GAINS_USDC_COLLATERAL_IDX = 2;

let gainsFeeCache: {
coinRoundTrip: Record<string, number>;
Expand Down Expand Up @@ -145,6 +147,7 @@ type GainsWalletData = {
events: number;
feesUsdc: number;
fundingFeesUsdc: number;
fundingEstimated: boolean;
borrowingFeesUsdc: number;
netCostUsdc: number;
positionSizeUsdc: number;
Expand Down Expand Up @@ -283,13 +286,13 @@ async function fetchGainsFeeRates(): Promise<{
// Borrow rate per second (v2 takes precedence over legacy)
const v2Borrow = v2BorrowParams[i]?.borrowingRatePerSecondP;
if (v2Borrow) {
borrowPerSecPerCoin[p.from] = parseFloat(v2Borrow) / GAINS_CARRY_PRECISION;
borrowPerSecPerCoin[p.from] = parseFloat(v2Borrow) / GAINS_BORROW_PRECISION;
}

// Funding rate per second (absolute value — longs and shorts may face same magnitude)
const fundingRate = fundingPairData[i]?.lastFundingRatePerSecondP;
if (fundingRate) {
fundingPerSecPerCoin[p.from] = Math.abs(parseFloat(fundingRate)) / GAINS_CARRY_PRECISION;
fundingPerSecPerCoin[p.from] = Math.abs(parseFloat(fundingRate)) / GAINS_FUNDING_PRECISION;
}
}

Expand Down Expand Up @@ -1046,11 +1049,24 @@ export async function GET(req: Request) {
}
}

// When the API doesn't return per-trade funding (meta absent or zero), fall back to
// the same per-second rate estimation used in the crossSim projection.
let fundingEstimated = false;
if (fundingFeesUsdc < 0.01 && Object.keys(gainsData.fundingPerSecPerCoin).length > 0) {
const gainsPositions = reconstructGainsPositions(usdcTrades, cutoffMs);
const est = estimateGainsFundingFees(gainsPositions, gainsData.fundingPerSecPerCoin);
if (est > 0.01) {
fundingFeesUsdc = est;
fundingEstimated = true;
}
}

const netCostUsdc = feesUsdc + fundingFeesUsdc + borrowingFeesUsdc;
walletData = {
events: usdcTrades.length,
feesUsdc,
fundingFeesUsdc,
fundingEstimated,
borrowingFeesUsdc,
netCostUsdc,
positionSizeUsdc: notionalUsd,
Expand Down
4 changes: 4 additions & 0 deletions src/components/fee-compare-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type GainsWalletData = {
events: number;
feesUsdc: number;
fundingFeesUsdc: number;
fundingEstimated: boolean;
borrowingFeesUsdc: number;
netCostUsdc: number;
positionSizeUsdc: number;
Expand Down Expand Up @@ -748,6 +749,9 @@ function WalletSide({
<div className="flex items-center justify-between">
<p className="text-[11px] text-ink-faint">
Funding {gW.fundingFeesUsdc > 0 ? "paid" : "received"}
{gW.fundingEstimated && (
<span className="ml-1 text-[9px] text-ink-faint/50 italic">est.</span>
)}
</p>
<p className={`font-mono text-xs font-semibold ${gW.fundingFeesUsdc > 0 ? "text-red-400" : "text-emerald-500"}`}>
{gW.fundingFeesUsdc > 0
Expand Down
Loading