"use client";
import { useState } from "react";
import {
ComposedChart, Bar, Cell, Line, XAxis, YAxis,
CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine,
} from "recharts";
import { TrendingUp, TrendingDown, Minus, ChevronDown, ChevronUp } from "lucide-react";
import type { Currency } from "@/lib/types";
import type { CotWeek, CotHistory } from "@/app/api/cot-history/route";
import { CURRENCY_META } from "@/lib/constants";
interface Props {
history: CotHistory | null;
loading: boolean;
}
const CURRENCIES: Currency[] = ["EUR", "GBP", "JPY", "AUD", "CAD", "NZD", "CHF", "USD"];
function formatNet(n: number): string {
const abs = Math.abs(n);
const sign = n >= 0 ? "+" : "-";
return abs >= 1000 ? `${sign}${(abs / 1000).toFixed(1)}k` : `${sign}${abs}`;
}
// ── Sparkline SVG ─────────────────────────────────────────────────────────────
function Sparkline({ weeks }: { weeks: CotWeek[] }) {
const pts = [...weeks].reverse().slice(-8);
if (pts.length < 2) return
—
;
const vals = pts.map(w => w.net);
const min = Math.min(...vals);
const max = Math.max(...vals);
const range = max - min || 1;
const W = 64, H = 24, PAD = 2;
const points = vals.map((v, i) => {
const x = PAD + (i / (vals.length - 1)) * (W - PAD * 2);
const y = H - PAD - ((v - min) / range) * (H - PAD * 2);
return `${x},${y}`;
}).join(" ");
const latest = vals[vals.length - 1];
const color = latest >= 0 ? "#10b981" : "#ef4444";
const lastX = PAD + (W - PAD * 2);
const lastY = H - PAD - ((latest - min) / range) * (H - PAD * 2);
return (
);
}
// ── Tooltip ───────────────────────────────────────────────────────────────────
function ChartTooltip({ active, payload, label }: {
active?: boolean;
payload?: Array<{ value: number; name: string }>;
label?: string;
}) {
if (!active || !payload?.length) return null;
const net = payload.find(p => p.name === "net")?.value ?? 0;
const longPct = payload.find(p => p.name === "longPct")?.value ?? 0;
const deltaNet = payload.find(p => p.name === "deltaNet")?.value;
return (
{label}
= 0 ? "text-emerald-400" : "text-red-400"}`}>Net : {formatNet(net)}
{longPct}% L / {100 - longPct}% S
{deltaNet !== undefined && deltaNet !== null && (
0 ? "text-emerald-400" : deltaNet < 0 ? "text-red-400" : "text-slate-500"}`}>
Δ semaine : {formatNet(deltaNet)}
)}
);
}
// ── Carte devise ──────────────────────────────────────────────────────────────
function CurrencyCard({ ccy, weeks, selected, onClick }: {
ccy: Currency; weeks: CotWeek[]; selected: boolean; onClick: () => void;
}) {
const latest = weeks[0];
const d = latest?.deltaNet ?? null;
const meta = CURRENCY_META[ccy];
const bias = latest ? (latest.longPct > 60 ? "bull" : latest.shortPct > 60 ? "bear" : "neu") : "neu";
return (
);
}
// ── Composant principal ───────────────────────────────────────────────────────
export default function CotTab({ history, loading }: Props) {
const [selected, setSelected] = useState(null);
const [mode, setMode] = useState<"tff" | "legacy">("tff");
if (loading) {
return Chargement historique COT…
;
}
if (!history || (!Object.keys(history.tff ?? {}).length && !Object.keys(history.legacy ?? {}).length)) {
return Données COT indisponibles
;
}
const dataset = history[mode] ?? {};
const latestDate = (dataset.EUR ?? dataset.GBP ?? [])[0]?.weekDate ?? "";
const handleSelect = (ccy: Currency) => setSelected(prev => prev === ccy ? null : ccy);
const selWeeks = selected ? (dataset[selected] ?? []) : [];
const chartData = [...selWeeks].reverse().map(w => ({
label: w.weekDate.slice(5),
net: w.net,
longPct: w.longPct,
deltaNet: w.deltaNet,
fill: w.net >= 0 ? "#10b981" : "#ef4444",
}));
const w0 = selWeeks[0];
const d = w0?.deltaNet ?? null;
const MODE_LABELS = {
tff: { label: "Hedge Funds (TFF)", desc: "Leveraged Money — gestionnaires spéculatifs, fonds macro" },
legacy: { label: "Non-Commercial (Legacy)", desc: "Tous spéculateurs — traders non-commerciaux (méthode classique depuis 1986)" },
};
return (
{/* En-tête + toggle */}
COT · CFTC
{latestDate && Semaine du {latestDate}}
{/* Toggle TFF / Legacy */}
{(["tff", "legacy"] as const).map(m => (
))}
{/* Description du mode */}
{MODE_LABELS[mode].desc}
{/* Grille 8 cartes */}
{CURRENCIES.map(ccy => (
handleSelect(ccy)}
/>
))}
{/* Panneau détail */}
{selected && chartData.length > 0 && (
{/* Résumé */}
{CURRENCY_META[selected]?.flag} {selected}
{w0 && (
= 0 ? "text-emerald-400" : "text-red-400"}`}>
{formatNet(w0.net)} net · {w0.longPct}%L / {w0.shortPct}%S
)}
{d !== null && (
0 ? "text-emerald-400" : d < 0 ? "text-red-400" : "text-slate-500"}`}>
{d > 0 ? "▲" : "▼"} {formatNet(Math.abs(d))} Δ sem.
)}
{w0?.deltaLong !== null && w0?.deltaLong !== undefined && (
+L {formatNet(w0.deltaLong)} / +S {formatNet(w0.deltaShort ?? 0)}
)}
{selWeeks.length} semaines
{/* Chart */}
`${(v/1000).toFixed(0)}k`} width={36} />
`${v}%`} width={30} />
} />
{chartData.map((e, i) => | )}
{/* Tableau 6 dernières semaines avec deltas */}
| Semaine |
Net |
Δ Net |
Δ Longs |
Δ Shorts |
%L |
{selWeeks.slice(0, 6).map((w, i) => (
| {w.weekDate} |
= 0 ? "text-emerald-400" : "text-red-400"}`}>
{formatNet(w.net)}
|
0 ? "text-emerald-400"
: w.deltaNet < 0 ? "text-red-400"
: "text-slate-500"
}`}>
{w.deltaNet !== null ? formatNet(w.deltaNet) : "—"}
|
0 ? "text-emerald-400" : "text-red-400"
}`}>
{w.deltaLong !== null && w.deltaLong !== undefined ? formatNet(w.deltaLong) : "—"}
|
0 ? "text-red-400" : "text-emerald-400"
}`}>
{w.deltaShort !== null && w.deltaShort !== undefined ? formatNet(w.deltaShort) : "—"}
|
{w.longPct}% |
))}
)}
);
}