mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-25 18:38:07 +00:00
auto: sync 2026-06-01 23:24
This commit is contained in:
+268
-45
@@ -2,19 +2,34 @@
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { TrendingUp, TrendingDown, Minus, ChevronDown, ChevronUp, Loader2, Database } from "lucide-react";
|
||||
import { CURRENCY_META } from "@/lib/constants";
|
||||
import { CURRENCY_META, COUNTRY_PROFILES } from "@/lib/constants";
|
||||
import { biasLabel, biasColor, calcMacroScore } from "@/lib/scoring";
|
||||
import { saveCache, loadCache, formatCacheDate } from "@/lib/localCache";
|
||||
import type { Currency, BiasPhase, RateExpectation } from "@/lib/types";
|
||||
import type { CBRatePath } from "@/lib/rateprobability";
|
||||
import type { SentimentEntry, CotEntry } from "@/lib/types";
|
||||
import NarrativeButton from "./NarrativeButton";
|
||||
|
||||
interface Ind { value: number | null; prev: number | null; surprise: number | null; trend: "up"|"down"|"flat"|null; lastUpdated: string | null }
|
||||
interface MacroData { currency: string; indicators: Record<string, Ind | null>; fetchedAt: string }
|
||||
interface MacroForecasts {
|
||||
cpi: number | null; cpiSurprise: number | null;
|
||||
unemployment: number | null; unemploymentSurprise: number | null;
|
||||
pmiMfg: number | null; pmiMfgSurprise: number | null;
|
||||
pmiSvc: number | null; pmiSvcSurprise: number | null;
|
||||
pmiComposite: number | null; pmiCompositeSurprise: number | null;
|
||||
retailSales: number | null; retailSalesSurprise: number | null;
|
||||
gdp: number | null; gdpSurprise: number | null;
|
||||
employment: number | null; employmentSurprise: number | null;
|
||||
}
|
||||
interface MacroData { currency: string; indicators: Record<string, Ind | null>; forecasts?: MacroForecasts | null; fetchedAt: string }
|
||||
|
||||
interface Props {
|
||||
currency: Currency;
|
||||
expectations: Record<string, unknown> | null;
|
||||
yields: { yields: Record<string, number | null>; spreads: Record<string, number | null> } | null;
|
||||
sentiment: SentimentEntry | null;
|
||||
cot: CotEntry | null;
|
||||
ratePath: CBRatePath | null;
|
||||
onDivergenceUpdate: (currency: Currency, score: number) => void;
|
||||
}
|
||||
|
||||
@@ -26,19 +41,30 @@ const PHASES: Record<BiasPhase, { label: string; color: string }> = {
|
||||
transition: { label: "🟠 Transition", color: "text-orange-500" },
|
||||
};
|
||||
|
||||
function SectionHeader({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 pt-1.5 pb-0.5">
|
||||
<span className="text-[8px] font-bold text-gray-300 uppercase tracking-widest whitespace-nowrap">{label}</span>
|
||||
<div className="flex-1 h-px bg-gray-100" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TrendIcon({ trend }: { trend: "up"|"down"|"flat"|null }) {
|
||||
if (trend === "up") return <TrendingUp size={11} className="text-green-500 flex-shrink-0" />;
|
||||
if (trend === "down") return <TrendingDown size={11} className="text-red-500 flex-shrink-0" />;
|
||||
return <Minus size={11} className="text-gray-300 flex-shrink-0" />;
|
||||
}
|
||||
|
||||
function Row({ label, ind, unit = "", invertSurprise = false, warn = false, consensus = null }: {
|
||||
label: string; ind: Ind | null; unit?: string; invertSurprise?: boolean; warn?: boolean; consensus?: number | null;
|
||||
function Row({ label, ind, unit = "", invertSurprise = false, warn = false, consensus = null, surpriseVsCons = null }: {
|
||||
label: string; ind: Ind | null; unit?: string; invertSurprise?: boolean; warn?: boolean;
|
||||
consensus?: number | null; // consensus à venir (upcoming)
|
||||
surpriseVsCons?: number | null; // actual − consensus si ≤5j post-release
|
||||
}) {
|
||||
const value = ind?.value ?? null;
|
||||
const prev = ind?.prev ?? null;
|
||||
|
||||
// Colorer la valeur actuelle selon la direction du mouvement
|
||||
// Colorer la valeur actuelle selon la direction du mouvement vs période précédente
|
||||
const s = ind?.surprise ?? null;
|
||||
const effectiveS = invertSurprise && s !== null ? -s : s;
|
||||
const valCls =
|
||||
@@ -50,6 +76,14 @@ function Row({ label, ind, unit = "", invertSurprise = false, warn = false, cons
|
||||
const fmt = (v: number | null) =>
|
||||
v !== null ? `${v.toFixed(2)}${unit}` : "—";
|
||||
|
||||
// Coloration de la surprise vs consensus (inversion pour chômage/unemployment)
|
||||
const effSurprise = invertSurprise && surpriseVsCons !== null ? -surpriseVsCons : surpriseVsCons;
|
||||
const surpriseCls = effSurprise === null ? ""
|
||||
: effSurprise > 0 ? "text-green-600"
|
||||
: effSurprise < 0 ? "text-red-600"
|
||||
: "text-gray-500";
|
||||
const surpriseArrow = effSurprise === null ? "" : effSurprise > 0 ? "▲" : effSurprise < 0 ? "▼" : "▬";
|
||||
|
||||
return (
|
||||
<div className="py-1.5 border-b border-gray-50 last:border-0">
|
||||
{/* Ligne 1 : label + valeur actuelle publiée */}
|
||||
@@ -64,23 +98,34 @@ function Row({ label, ind, unit = "", invertSurprise = false, warn = false, cons
|
||||
{fmt(value)}
|
||||
</span>
|
||||
</div>
|
||||
{/* Ligne 2 : précédent + consensus marché */}
|
||||
{/* Ligne 2 : précédent + consensus à venir OU surprise post-publication */}
|
||||
<div className="flex items-center justify-between pl-5 mt-0.5">
|
||||
<span className="text-[10px] text-gray-400 tabular-nums">
|
||||
Préc. <span className="text-gray-500 font-medium">{fmt(prev)}</span>
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400 tabular-nums">
|
||||
Cons.
|
||||
{consensus !== null
|
||||
? <span className="text-blue-500 font-medium">{fmt(consensus)}</span>
|
||||
: <span className="text-gray-300">—</span>}
|
||||
</span>
|
||||
{surpriseVsCons !== null ? (
|
||||
// Surprise vs consensus (≤5 jours post-release)
|
||||
<span className="text-[10px] tabular-nums">
|
||||
<span className="text-gray-400">Surpr. </span>
|
||||
<span className={`font-medium ${surpriseCls}`}>
|
||||
{surpriseArrow}{surpriseVsCons > 0 ? "+" : ""}{surpriseVsCons.toFixed(2)}{unit}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
// Consensus à venir (upcoming)
|
||||
<span className="text-[10px] text-gray-400 tabular-nums">
|
||||
Cons.
|
||||
{consensus !== null
|
||||
? <span className="text-blue-500 font-medium">{fmt(consensus)}</span>
|
||||
: <span className="text-gray-300">—</span>}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CurrencyCard({ currency, expectations, yields, onDivergenceUpdate }: Props) {
|
||||
export default function CurrencyCard({ currency, expectations, yields, sentiment, cot, ratePath, onDivergenceUpdate }: Props) {
|
||||
const meta = CURRENCY_META[currency];
|
||||
const [data, setData] = useState<MacroData | null>(null);
|
||||
const [phase, setPhase] = useState<BiasPhase>("hawkish_pause");
|
||||
@@ -151,14 +196,15 @@ export default function CurrencyCard({ currency, expectations, yields, onDiverge
|
||||
setRateExp(all.find((e) => e.cb.toLowerCase().includes(cbShort) || e.cb.toLowerCase().includes(currency.toLowerCase())) ?? null);
|
||||
}, [expectations, currency, meta.cbShort]);
|
||||
|
||||
const inds = data?.indicators;
|
||||
const inds = data?.indicators;
|
||||
const fc = data?.forecasts ?? null; // ForexFactory forecasts
|
||||
|
||||
// Build a minimal indicator object for scoring
|
||||
const forScoring = {
|
||||
policyRate: { value: inds?.policyRate?.value ?? null, prev: inds?.policyRate?.prev ?? null, consensus: null, surprise: inds?.policyRate?.surprise ?? null, trend: inds?.policyRate?.trend ?? null, lastUpdated: "" },
|
||||
cpiCore: { value: inds?.cpiCore?.value ?? null, prev: inds?.cpiCore?.prev ?? null, consensus: null, surprise: inds?.cpiCore?.surprise ?? null, trend: inds?.cpiCore?.trend ?? null, lastUpdated: "" },
|
||||
pmiMfg: { value: null, prev: null, consensus: null, surprise: null, trend: null, lastUpdated: "" },
|
||||
pmiServices: { value: null, prev: null, consensus: null, surprise: null, trend: null, lastUpdated: "" },
|
||||
pmiMfg: { value: inds?.pmiMfg?.value ?? null, prev: inds?.pmiMfg?.prev ?? null, consensus: null, surprise: inds?.pmiMfg?.surprise ?? null, trend: inds?.pmiMfg?.trend ?? null, lastUpdated: "" },
|
||||
pmiServices: { value: inds?.pmiServices?.value ?? null, prev: inds?.pmiServices?.prev ?? null, consensus: null, surprise: inds?.pmiServices?.surprise ?? null, trend: inds?.pmiServices?.trend ?? null, lastUpdated: "" },
|
||||
gdp: { value: inds?.gdp?.value ?? null, prev: inds?.gdp?.prev ?? null, consensus: null, surprise: inds?.gdp?.surprise ?? null, trend: inds?.gdp?.trend ?? null, lastUpdated: "" },
|
||||
retailSales: { value: inds?.retailSales?.value ?? null, prev: inds?.retailSales?.prev ?? null, consensus: null, surprise: inds?.retailSales?.surprise ?? null, trend: inds?.retailSales?.trend ?? null, lastUpdated: "" },
|
||||
unemployment: { value: inds?.unemployment?.value ?? null, prev: inds?.unemployment?.prev ?? null, consensus: null, surprise: inds?.unemployment?.surprise ?? null, trend: inds?.unemployment?.trend ?? null, lastUpdated: "" },
|
||||
@@ -173,6 +219,30 @@ export default function CurrencyCard({ currency, expectations, yields, onDiverge
|
||||
const spread10Y = yields?.spreads[currency] ?? null;
|
||||
const borderCls = macroScore >= 4 ? "border-green-200" : macroScore <= -4 ? "border-red-200" : "border-gray-200";
|
||||
|
||||
// Consensus = taux attendu à la prochaine réunion CB
|
||||
// Priorité : ratePath (OIS temps réel) > rateExp (snapshot statique)
|
||||
const rateConsensus = (() => {
|
||||
const rate = inds?.policyRate?.value ?? null;
|
||||
if (rate === null) return null;
|
||||
// OIS live
|
||||
if (ratePath && ratePath.meetings.length > 0) {
|
||||
const next = ratePath.meetings[0];
|
||||
if (next.probMovePct > 50) {
|
||||
return next.probIsCut
|
||||
? parseFloat((rate - 0.25).toFixed(2))
|
||||
: parseFloat((rate + 0.25).toFixed(2));
|
||||
}
|
||||
return parseFloat(rate.toFixed(2));
|
||||
}
|
||||
// Fallback snapshot statique (CHF / si rateprobability indispo)
|
||||
if (!rateExp) return null;
|
||||
const desc = rateExp.prob_desc.toLowerCase();
|
||||
if (desc.includes("no change")) return parseFloat(rate.toFixed(2));
|
||||
if (rateExp.direction === "cut" && rateExp.prob_pct > 50) return parseFloat((rate - 0.25).toFixed(2));
|
||||
if (rateExp.direction === "hike" && rateExp.prob_pct > 50) return parseFloat((rate + 0.25).toFixed(2));
|
||||
return parseFloat(rate.toFixed(2));
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className={`bg-white border rounded-xl overflow-hidden ${borderCls}`}>
|
||||
{/* Header */}
|
||||
@@ -206,49 +276,202 @@ export default function CurrencyCard({ currency, expectations, yields, onDiverge
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rate expectation pill */}
|
||||
{rateExp && (
|
||||
{/* ── Probabilités OIS (rateprobability.com) ─────────────────────────── */}
|
||||
{ratePath && ratePath.meetings.length > 0 ? (
|
||||
<div className="mx-4 mb-2 px-3 py-2 bg-gray-50 rounded-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[9px] font-semibold text-gray-400 uppercase tracking-wider">OIS · marchés</span>
|
||||
<span className="text-[9px] text-gray-400">au {ratePath.asOf}</span>
|
||||
</div>
|
||||
|
||||
{/* Pic + taux fin d'année */}
|
||||
{ratePath.peakMeeting && (
|
||||
<div className="flex items-center justify-between text-[10px] mb-1.5">
|
||||
<span>
|
||||
<span className="text-gray-500">Pic </span>
|
||||
<span className="font-semibold text-gray-800">{ratePath.peakMeeting.label}</span>
|
||||
<span className={`ml-1 font-bold ${ratePath.peakMeeting.probIsCut ? "text-green-600" : "text-red-500"}`}>
|
||||
{ratePath.peakMeeting.probMovePct.toFixed(0)}%
|
||||
{ratePath.peakMeeting.probIsCut ? " ▼" : " ▲"}
|
||||
{ratePath.peakMeeting.changeBps > 0.5 &&
|
||||
<span className="font-normal text-gray-400"> +{ratePath.peakMeeting.changeBps.toFixed(0)}bps</span>
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
{ratePath.yearEndImplied !== null && (
|
||||
<span className="text-gray-400 text-[9px]">
|
||||
fin d'an {ratePath.yearEndImplied.toFixed(2)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timeline réunion par réunion */}
|
||||
<div className="flex flex-col gap-[3px]">
|
||||
{ratePath.meetings.slice(0, 6).map(m => (
|
||||
<div key={m.dateIso} className="flex items-center gap-1.5">
|
||||
<span className="text-[9px] text-gray-400 w-11 shrink-0 tabular-nums">{m.label}</span>
|
||||
<div className="flex-1 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${m.probIsCut ? "bg-green-400" : "bg-red-400"}`}
|
||||
style={{ width: `${m.probMovePct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className={`text-[9px] w-7 text-right tabular-nums shrink-0 ${m.probMovePct >= 50 ? "font-bold text-gray-800" : "text-gray-400"}`}>
|
||||
{m.probMovePct.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : rateExp ? (
|
||||
/* Fallback snapshot statique (ex: CHF sans données OIS) */
|
||||
<div className="mx-4 mb-2 px-3 py-1.5 bg-gray-50 rounded-lg text-xs leading-snug">
|
||||
<span className="font-semibold">{rateExp.direction === "hike" ? "▲" : "▼"} {rateExp.bps} bps</span>
|
||||
<span className="relative group inline-flex items-center ml-0.5 cursor-help align-middle">
|
||||
<span className="text-[9px] text-gray-400 border border-gray-300 rounded-full w-3 h-3 flex items-center justify-center leading-none select-none">i</span>
|
||||
<span className="pointer-events-none absolute bottom-full left-0 mb-1.5 hidden group-hover:block bg-gray-800 text-white text-[10px] rounded px-2 py-1.5 w-56 z-50 leading-snug shadow-lg whitespace-normal">
|
||||
1 bp (basis point) = 0,01% de taux. Variation cumulée attendue d'ici fin d'année selon les marchés (OIS / futures de taux). Distinct de la probabilité à la prochaine réunion ci-dessous.
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-gray-500"> · {rateExp.prob_pct}% prob. </span>
|
||||
<span className={`font-medium ${rateExp.prob_desc.includes("no change") || rateExp.prob_desc.includes("sans") ? "text-gray-600" : rateExp.direction === "hike" ? "text-red-600" : "text-green-600"}`}>
|
||||
<span className={`font-medium ${rateExp.prob_desc.includes("no change") ? "text-gray-600" : rateExp.direction === "hike" ? "text-red-600" : "text-green-600"}`}>
|
||||
{rateExp.prob_desc}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* Core indicators (always visible) */}
|
||||
{/* ── Indicateurs macro — organisation "prisme" ─────────────────────── */}
|
||||
<div className="px-4 pb-2">
|
||||
<Row label="Taux directeur" ind={inds?.policyRate ?? null} unit="%" />
|
||||
<Row label="CPI (MoM%)" ind={inds?.cpiCore ?? null} unit="%" />
|
||||
<Row label="PIB (QoQ %)" ind={inds?.gdp ?? null} unit="%" />
|
||||
<Row label="Chômage" ind={inds?.unemployment ?? null} unit="%" invertSurprise />
|
||||
|
||||
{/* Expanded indicators */}
|
||||
{/* ── POLITIQUE MONÉTAIRE ─────────────────────────────────────────── */}
|
||||
<SectionHeader label="Politique monétaire" />
|
||||
<Row label="Taux directeur" ind={inds?.policyRate ?? null} unit="%" consensus={rateConsensus} />
|
||||
<div className="flex items-center justify-between py-1.5 text-xs border-b border-gray-50">
|
||||
<span className="text-gray-500 text-xs">10Y Yield</span>
|
||||
<span className="font-semibold text-gray-800 tabular-nums text-xs">
|
||||
{yield10Y !== null ? `${yield10Y.toFixed(2)}%` : "—"}
|
||||
{spread10Y !== null && (
|
||||
<span className={`ml-1 text-[10px] ${spread10Y > 0 ? "text-green-600" : "text-red-600"}`}>
|
||||
({spread10Y > 0 ? "+" : ""}{spread10Y}bps vs US)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ── INFLATION ───────────────────────────────────────────────────── */}
|
||||
<SectionHeader label="Inflation" />
|
||||
<Row label="CPI Core YoY" ind={inds?.cpiCore ?? null} unit="%" consensus={fc?.cpi ?? null} surpriseVsCons={fc?.cpiSurprise ?? null} />
|
||||
<Row label="CPI MoM" ind={inds?.cpiMoM ?? null} unit="%" />
|
||||
|
||||
{/* ── CROISSANCE ──────────────────────────────────────────────────── */}
|
||||
<SectionHeader label="Croissance" />
|
||||
<Row label="PIB (QoQ%)" ind={inds?.gdp ?? null} unit="%" consensus={fc?.gdp ?? null} surpriseVsCons={fc?.gdpSurprise ?? null} />
|
||||
<Row label="PMI Composite" ind={inds?.pmiComposite ?? null} warn={!inds?.pmiComposite} consensus={fc?.pmiComposite ?? null} surpriseVsCons={fc?.pmiCompositeSurprise ?? null} />
|
||||
|
||||
{/* ── EMPLOI ──────────────────────────────────────────────────────── */}
|
||||
<SectionHeader label="Emploi" />
|
||||
{/* Variation emploi = NFP/Employment Change en milliers — ex: +115k = 115 000 emplois créés */}
|
||||
<Row label="Variation emploi" ind={inds?.employment ?? null} unit="k" warn={!inds?.employment} consensus={fc?.employment ?? null} surpriseVsCons={fc?.employmentSurprise ?? null} />
|
||||
<Row label="Taux de chômage" ind={inds?.unemployment ?? null} unit="%" invertSurprise consensus={fc?.unemployment ?? null} surpriseVsCons={fc?.unemploymentSurprise ?? null} />
|
||||
|
||||
{/* ── Données supplémentaires (expanded) ──────────────────────────── */}
|
||||
{expanded && (
|
||||
<>
|
||||
<Row label="PMI Mfg" ind={null} warn />
|
||||
<Row label="PMI Services" ind={null} warn />
|
||||
<Row label="Retail Sales" ind={inds?.retailSales ?? null} unit="%" warn={!inds?.retailSales} />
|
||||
<Row label="Emploi (MoM%)" ind={inds?.employment ?? null} unit="%" warn={!inds?.employment} />
|
||||
{/* 10Y yield */}
|
||||
<div className="flex items-center justify-between py-1.5 text-xs">
|
||||
<span className="text-gray-500">10Y Yield</span>
|
||||
<span className="font-semibold text-gray-800 tabular-nums">
|
||||
{yield10Y !== null ? `${yield10Y.toFixed(2)}%` : "—"}
|
||||
{spread10Y !== null && (
|
||||
<span className={`ml-1 text-[10px] ${spread10Y > 0 ? "text-green-600" : "text-red-600"}`}>
|
||||
({spread10Y > 0 ? "+" : ""}{spread10Y}bps)
|
||||
{/* PMI détail */}
|
||||
<SectionHeader label="PMI détail" />
|
||||
<Row label="PMI Mfg" ind={inds?.pmiMfg ?? null} warn={!inds?.pmiMfg} consensus={fc?.pmiMfg ?? null} surpriseVsCons={fc?.pmiMfgSurprise ?? null} />
|
||||
<Row label="PMI Services" ind={inds?.pmiServices ?? null} warn={!inds?.pmiServices} consensus={fc?.pmiSvc ?? null} surpriseVsCons={fc?.pmiSvcSurprise ?? null} />
|
||||
<Row label="Ventes détail" ind={inds?.retailSales ?? null} unit="%" warn={!inds?.retailSales} consensus={fc?.retailSales ?? null} surpriseVsCons={fc?.retailSalesSurprise ?? null} />
|
||||
|
||||
{/* Géopolitique */}
|
||||
<SectionHeader label="Géopolitique" />
|
||||
{inds?.tradeBalance ? (
|
||||
<div className="flex items-center justify-between py-1.5 border-b border-gray-50">
|
||||
<span className="text-gray-500 text-xs">Balance comm.</span>
|
||||
<span className={`text-xs font-semibold tabular-nums ${(inds.tradeBalance.value ?? 0) >= 0 ? "text-green-600" : "text-red-500"}`}>
|
||||
{(inds.tradeBalance.value ?? 0) >= 0 ? "+" : ""}{inds.tradeBalance.value?.toFixed(1)}B
|
||||
<span className="text-[9px] text-gray-400 font-normal ml-0.5">
|
||||
{(inds.tradeBalance.value ?? 0) >= 0 ? " surplus" : " déficit"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{/* Profil énergie + matières premières */}
|
||||
{(() => {
|
||||
const profile = COUNTRY_PROFILES[currency];
|
||||
if (!profile) return null;
|
||||
const energyColor = profile.energy === "exporter" ? "text-green-700 bg-green-50" : profile.energy === "importer" ? "text-red-700 bg-red-50" : "text-gray-600 bg-gray-100";
|
||||
const energyLabel = profile.energy === "exporter" ? "🛢 Export. énergie" : profile.energy === "importer" ? "⚡ Import. énergie" : "⚖ Énergie ~neutre";
|
||||
return (
|
||||
<div className="py-1.5 border-b border-gray-50 space-y-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className={`text-[9px] font-semibold px-1.5 py-0.5 rounded ${energyColor}`}>{energyLabel}</span>
|
||||
<span className="text-[9px] text-gray-400 text-right leading-tight max-w-[55%]">{profile.energyNote}</span>
|
||||
</div>
|
||||
{profile.commodities.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 pt-0.5">
|
||||
{profile.commodities.map(c => (
|
||||
<span key={c} className="text-[8px] bg-amber-50 text-amber-700 border border-amber-200 px-1.5 py-0.5 rounded-full">{c}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* ── Sentiment & Positionnement ──────────────────────────────── */}
|
||||
<SectionHeader label="Sentiment & Positionnement" />
|
||||
{sentiment ? (
|
||||
<div className="py-1.5 border-b border-gray-50">
|
||||
<div className="flex items-center justify-between text-xs mb-1">
|
||||
<span className="text-gray-500 text-[10px]">{sentiment.pair} · Myfxbook</span>
|
||||
<span className="text-[10px] tabular-nums">
|
||||
<span className="text-green-600 font-semibold">{sentiment.longPct}% L</span>
|
||||
<span className="text-gray-300 mx-0.5">/</span>
|
||||
<span className="text-red-500 font-semibold">{sentiment.shortPct}% S</span>
|
||||
</span>
|
||||
</div>
|
||||
{/* Barre visuelle long/short */}
|
||||
<div className="flex h-1.5 rounded-full overflow-hidden">
|
||||
<div className="bg-green-400 transition-all" style={{ width: `${sentiment.longPct}%` }} />
|
||||
<div className="bg-red-400 flex-1" />
|
||||
</div>
|
||||
{/* Signal contrarien */}
|
||||
{(sentiment.longPct >= 70 || sentiment.shortPct >= 70) && (
|
||||
<p className={`text-[9px] mt-0.5 font-medium ${sentiment.longPct >= 70 ? "text-red-500" : "text-green-600"}`}>
|
||||
{sentiment.longPct >= 70 ? "⚠ Retail très long — signal contrarien baissier" : "⚠ Retail très short — signal contrarien haussier"}
|
||||
</p>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1 text-[10px] text-gray-300 border-b border-gray-50">— (Myfxbook indisponible)</div>
|
||||
)}
|
||||
|
||||
{/* ── COT CFTC ────────────────────────────────────────────────── */}
|
||||
<div className="pt-1 pb-0.5">
|
||||
<span className="text-[9px] font-semibold text-gray-400 uppercase tracking-wider">COT — Hedge Funds</span>
|
||||
</div>
|
||||
{cot ? (
|
||||
<div className="py-1.5">
|
||||
<div className="flex items-center justify-between text-xs mb-1">
|
||||
<span className="text-gray-500 text-[10px]">Lev. Money · {cot.weekDate}</span>
|
||||
<span className="text-[10px] tabular-nums">
|
||||
<span className="text-green-600 font-semibold">{cot.longPct}% L</span>
|
||||
<span className="text-gray-300 mx-0.5">/</span>
|
||||
<span className="text-red-500 font-semibold">{cot.shortPct}% S</span>
|
||||
<span className="text-gray-400 ml-1">({cot.net > 0 ? "+" : ""}{cot.net.toLocaleString("fr-FR")})</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex h-1.5 rounded-full overflow-hidden">
|
||||
<div className="bg-green-400 transition-all" style={{ width: `${cot.longPct}%` }} />
|
||||
<div className="bg-red-400 flex-1" />
|
||||
</div>
|
||||
{/* Divergence COT vs Sentiment */}
|
||||
{sentiment && Math.abs(cot.longPct - sentiment.longPct) >= 20 && (
|
||||
<p className="text-[9px] mt-0.5 text-amber-600 font-medium">
|
||||
⚡ Divergence COT/Retail : {Math.abs(cot.longPct - sentiment.longPct)}pts
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1 text-[10px] text-gray-300">— (CFTC indisponible)</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user