From 07e195545b421d834a7aa14a23134dc5c1e67d3d Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:10:42 +0200 Subject: [PATCH 1/3] fix: include long-running Gains positions in HL funding projection --- src/app/api/fee-compare/route.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index d6d53301..1ba083d7 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -1130,10 +1130,12 @@ function reconstructGainsPositions(trades: GainsApiTrade[], cutoffMs: number): P const slices: PositionSlice[] = []; for (const { open, close } of byId.values()) { if (!open) continue; - const openMs = new Date(open.date).getTime(); - if (openMs < cutoffMs) continue; - // Still-open positions use now as close time (same as reconstructHlPositions) + const rawOpenMs = new Date(open.date).getTime(); const closeMs = close ? new Date(close.date).getTime() : now; + // Skip positions that closed before the analysis window + if (closeMs < cutoffMs) continue; + // Cap openMs to the window start so long-running positions aren't skipped + const openMs = Math.max(rawOpenMs, cutoffMs); slices.push({ coin: open.pair.split("/")[0], notionalUsd: open.size * open.leverage, From 524bcdfc15621dfa7d0460bc261e8d8421cb83d8 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:13:21 +0200 Subject: [PATCH 2/3] fix: use earliest POSSIZEINCREASE as anchor for positions opened before fetch window --- src/app/api/fee-compare/route.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index 1ba083d7..11b7236e 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -1115,33 +1115,40 @@ function reconstructGainsPositions(trades: GainsApiTrade[], cutoffMs: number): P const INCREASE_ACTIONS = new Set(["TradePosSizeIncrease"]); const CLOSE_ACTIONS = new Set(["TradeClosedMarket", "TradeClosedTP", "TradeClosedSL", "TradeClosedLIQ"]); - const byId = new Map(); + const byId = new Map(); for (const t of trades) { if (!byId.has(t.id)) byId.set(t.id, {}); const e = byId.get(t.id)!; if (OPEN_ACTIONS.has(t.action)) e.open = t; - else if (INCREASE_ACTIONS.has(t.action) && e.open) { - e.open = { ...e.open, size: t.size, leverage: t.leverage }; + else if (INCREASE_ACTIONS.has(t.action)) { + if (e.open) e.open = { ...e.open, size: t.size, leverage: t.leverage }; + // Track increases for positions opened before the window (no open event in data) + else if (!e.lastIncrease || new Date(t.date).getTime() < new Date(e.lastIncrease.date).getTime()) { + e.lastIncrease = t; + } } else if (CLOSE_ACTIONS.has(t.action)) e.close = t; } const now = Date.now(); const slices: PositionSlice[] = []; - for (const { open, close } of byId.values()) { - if (!open) continue; - const rawOpenMs = new Date(open.date).getTime(); + for (const { open, close, lastIncrease } of byId.values()) { + // Use open event if available; fall back to earliest increase in the window + // for positions opened before the fetch window (open event not in data). + const anchor = open ?? lastIncrease; + if (!anchor) continue; + const rawOpenMs = new Date(anchor.date).getTime(); const closeMs = close ? new Date(close.date).getTime() : now; // Skip positions that closed before the analysis window if (closeMs < cutoffMs) continue; - // Cap openMs to the window start so long-running positions aren't skipped + // Cap openMs to the window start so long-running positions aren't missed const openMs = Math.max(rawOpenMs, cutoffMs); slices.push({ - coin: open.pair.split("/")[0], - notionalUsd: open.size * open.leverage, + coin: anchor.pair.split("/")[0], + notionalUsd: anchor.size * anchor.leverage, openMs, closeMs, - isLong: open.buy !== false, + isLong: anchor.buy !== false, }); } From 172e4e8bf0b07003a3a2ecb2040ac651afd904a4 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:19:26 +0200 Subject: [PATCH 3/3] fix: fetch 1yr Gains history for position reconstruction to find true open timestamps --- src/app/api/fee-compare/route.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index 11b7236e..6c579c46 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -1489,8 +1489,11 @@ export async function GET(req: Request) { await Promise.all(fetches); - // Phase 2: fetch HL funding history for Gains positions (Gains→HL carry projection) + // Phase 2: fetch HL funding history + extended Gains history for position reconstruction let hlFundingHistoryByCoins: Map> = new Map(); + // Extended Gains history (1 year) used only for HL funding projection reconstruction — + // the fee accounting (taker/borrow/funding fees) still uses gainsTradesData (cutoffMs window). + let gainsPositionData: GainsApiTrade[] = gainsTradesData; if ( fetchEvmWallet && (venueA === "gains" || venueB === "gains") && @@ -1503,7 +1506,13 @@ export async function GET(req: Request) { .map((t) => t.pair.split("/")[0]) ); const coinsToFetch = [...gainsCoinSet].slice(0, 6); - hlFundingHistoryByCoins = await fetchHlFundingHistory(coinsToFetch, cutoffMs).catch(() => new Map()); + const extendedCutoffMs = cutoffMs - 365 * 24 * 60 * 60 * 1000; + const [fundingHistory, extendedTrades] = await Promise.all([ + fetchHlFundingHistory(coinsToFetch, cutoffMs).catch(() => new Map>()), + fetchGainsTrades(wallet, extendedCutoffMs).catch(() => gainsTradesData), + ]); + hlFundingHistoryByCoins = fundingHistory; + gainsPositionData = extendedTrades; } function buildVenueResult(slug: string, rate: number, note: string, rateIsLive: boolean): VenueResult { @@ -1672,9 +1681,9 @@ export async function GET(req: Request) { hlOpenPositions, cutoffMs ); - } else if (venueA === "gains" && gainsTradesData.length > 0) { + } else if (venueA === "gains" && gainsPositionData.length > 0) { positions = reconstructGainsPositions( - gainsTradesData.filter((t) => t.collateralIndex === 3), + gainsPositionData.filter((t) => t.collateralIndex === 3), cutoffMs ); } else if (venueA === "gmx-v2" && gmxWalletData) { @@ -1812,9 +1821,9 @@ export async function GET(req: Request) { hlOpenPositions, cutoffMs ); - } else if (venueB === "gains" && gainsTradesData.length > 0) { + } else if (venueB === "gains" && gainsPositionData.length > 0) { positions = reconstructGainsPositions( - gainsTradesData.filter((t) => t.collateralIndex === 3), + gainsPositionData.filter((t) => t.collateralIndex === 3), cutoffMs ); } else if (venueB === "gmx-v2" && gmxWalletData) {