← Back
import { useState, useEffect, useCallback, useRef } from 'react';

export interface WhaleProfile {
  address: string;
  label: string | null;
  totalTrades: number;
  totalVolume: number;
  avgTradeSize: number;
  largestTrade: number;
  winRate: number | null;
  topCategory: string | null;
  firstSeen: string | null;
  lastSeen: string | null;
  totalPnl: number | null;
  avgBuy: number | null;
  sellRate: number | null;
  resolvedTrades: number;
}

export interface WhaleProfileTrade {
  id: string;
  marketId: string;
  marketQuestion: string | null;
  marketSlug: string | null;
  side: string;
  outcome: string;
  size: number;
  price: number;
  amount: number;
  timestamp: string;
  status: 'won' | 'lost' | 'open' | 'resolved';
  pnl: number | null;   // realized PnL (resolved BUY only), null otherwise
  won: boolean | null;
}

export interface WhaleTopMarket {
  marketId: string;
  marketQuestion: string | null;
  trades: number;
  volume: number;
}

export interface WhaleEquityPoint {
  t: string;       // ISO timestamp of the resolved trade
  pnl: number;     // realized PnL of that trade
  cum: number;     // running cumulative realized PnL
}

interface WhaleProfileData {
  profile: WhaleProfile;
  trades: WhaleProfileTrade[];
  topMarkets: WhaleTopMarket[];
  equity: WhaleEquityPoint[];
}

export function useWhaleProfile(address: string | null, tradeLimit = 50, offset = 0) {
  const [data, setData] = useState<WhaleProfileData | null>(null);
  const [loading, setLoading] = useState(false);
  const [backfilling, setBackfilling] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchProfile = useCallback(async () => {
    if (!address) return;
    setLoading(true);
    setError(null);

    try {
      const res = await fetch(
        `/api/signals/whale/${encodeURIComponent(address)}?limit=${tradeLimit}&offset=${offset}`
      );
      const d = await res.json();
      if (d.success) {
        setData(d.data ?? null);
      } else {
        setError(d.error ?? 'Whale not found');
        setData(null);
      }
    } catch {
      setError('Failed to fetch whale profile');
      setData(null);
    } finally {
      setLoading(false);
    }
  }, [address, tradeLimit, offset]);

  useEffect(() => {
    const t = setTimeout(fetchProfile, 0);
    return () => { clearTimeout(t); };
  }, [fetchProfile]);

  // Keep a stable ref to the latest fetchProfile so the backfill effect can
  // refetch the current page without re-running on every page change.
  const fetchRef = useRef(fetchProfile);
  useEffect(() => { fetchRef.current = fetchProfile; }, [fetchProfile]);

  // On open (address change only), backfill this whale's full history (Data API
  // user filter) so the profile shows hundreds of trades + a meaningful win-rate
  // instead of the few caught globally. Then refetch the current page.
  useEffect(() => {
    if (!address) return;
    let cancelled = false;
    (async () => {
      setBackfilling(true);
      try {
        await fetch(
          `/api/signals/whale/${encodeURIComponent(address)}/backfill`,
          { method: 'POST' }
        );
        if (!cancelled) await fetchRef.current();
      } catch { /* ignore — keep showing whatever we already have */ }
      finally { if (!cancelled) setBackfilling(false); }
    })();
    return () => { cancelled = true; };
  }, [address]);

  return {
    profile: data?.profile ?? null,
    trades: data?.trades ?? [],
    topMarkets: data?.topMarkets ?? [],
    equity: data?.equity ?? [],
    loading,
    backfilling,
    error,
    refresh: fetchProfile,
  };
}

📜 Git History

03a2d80chore(save): session 2026-06-22 — Phase2 redesign + 3mo history + realized-PnL fix; staged _impl edits11 days ago
Show last diff
Loading...