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

/**
 * Phase E: for a set of whale addresses, fetch "what if we copied them under our
 * config" backtest (GET /api/signals/whale/{addr}/backtest) and index by address.
 * Lets the Leaders list show an expected PF/PnL badge so a whale's *copy-fit* —
 * not its raw lifetime PnL — drives the choice (raw stats lie, e.g. Sociable-Rooster
 * $85K lifetime but PF 0.24 under our config).
 */
export interface Backtest {
  copies: number;
  winRate: number;
  pnl: number;
  pf: number | null;   // null = no losing copies
  roi: number;
  perDay: number;
}

export function useBacktests(addresses: string[], days = 30, size = 2) {
  // Stable key so the effect only re-runs when the actual set changes.
  const key = addresses.map(a => a.toLowerCase()).sort().join(',');
  const [map, setMap] = useState<Record<string, Backtest>>({});

  useEffect(() => {
    if (!key) return;
    const addrs = key.split(',');
    // AbortController: leaving the page (e.g. opening a whale profile) cancels all
    // in-flight backtest fetches, freeing the browser's ~6 connections so the profile
    // GET isn't starved. CONCURRENCY caps how many run at once (defense + lighter API).
    const ctrl = new AbortController();
    const CONCURRENCY = 8;
    (async () => {
      const next: Record<string, Backtest> = {};
      for (let i = 0; i < addrs.length; i += CONCURRENCY) {
        if (ctrl.signal.aborted) return;
        const batch = addrs.slice(i, i + CONCURRENCY);
        const results = await Promise.allSettled(batch.map(a =>
          fetch(`/api/signals/whale/${a}/backtest?days=${days}&size=${size}&pmin=5&pmax=95`, { signal: ctrl.signal })
            .then(r => r.ok ? r.json() : null)
            .then(d => ({ a, d }))
        ));
        for (const res of results) {
          if (res.status !== 'fulfilled' || !res.value?.d?.data) continue;
          const { a, d } = res.value;
          const v = d.data;
          next[a] = {
            copies: Number(v.copies) || 0,
            winRate: Number(v.winRate) || 0,
            pnl: Number(v.pnl) || 0,
            pf: v.pf == null ? null : Number(v.pf),
            roi: Number(v.roi) || 0,
            perDay: Number(v.perDay) || 0,
          };
        }
      }
      if (!ctrl.signal.aborted) setMap(next);
    })();
    return () => ctrl.abort();
  }, [key, days, size]);

  return key ? map : {};
}

📜 Git History

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