"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 ( {min < 0 && max > 0 && ( )} ); } // ── 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 */} {selWeeks.slice(0, 6).map((w, i) => ( ))}
Semaine Net Δ Net Δ Longs Δ Shorts %L
{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}%
)}
); }