mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-16 14:08:06 +00:00
feat: dashboard v8 — COT, FX Weekly, Report, TvChart, Sentiment, News
Ajout des onglets COT, Weekly Report, TvChart. Refonte Sentiment, News, Calendar, Drivers. Nouvelles API cot-history et fx-weekly. Intégration FinancialJuice. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
e11d61eb4d
commit
1a2ea02b22
@@ -0,0 +1,174 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { COT_CODES } from "@/lib/constants";
|
||||
import type { Currency } from "@/lib/types";
|
||||
|
||||
const SODA_BASE = "https://publicreporting.cftc.gov/resource";
|
||||
const CODES_LIST = Object.values(COT_CODES).map(c => `'${c}'`).join(",");
|
||||
const WHERE = `cftc_contract_market_code in(${CODES_LIST}) AND futonly_or_combined='FutOnly'`;
|
||||
const ORDER = "report_date_as_yyyy_mm_dd DESC";
|
||||
const LIMIT = 250; // 8 devises × 26 semaines = 208 lignes max
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CotWeek {
|
||||
weekDate: string;
|
||||
net: number;
|
||||
longPct: number;
|
||||
shortPct: number;
|
||||
totalLev: number;
|
||||
deltaNet: number | null; // changement net semaine en semaine (depuis l'API)
|
||||
deltaLong: number | null; // contrats longs ajoutés/retirés
|
||||
deltaShort: number | null; // contrats shorts ajoutés/retirés
|
||||
}
|
||||
|
||||
export interface CotHistory {
|
||||
tff: Record<Currency, CotWeek[]>; // Leveraged Money — Hedge Funds
|
||||
legacy: Record<Currency, CotWeek[]>; // Non-Commercial — tous spéculateurs
|
||||
}
|
||||
|
||||
// ── Cache ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
let _cache: { data: CotHistory; ts: number } | null = null;
|
||||
|
||||
function cacheTtl(): number {
|
||||
const d = new Date();
|
||||
const daysUntilFriday = (5 - d.getUTCDay() + 7) % 7 || 7;
|
||||
d.setUTCDate(d.getUTCDate() + daysUntilFriday);
|
||||
d.setUTCHours(15, 30, 0, 0);
|
||||
return Math.min(d.getTime() - Date.now(), 4 * 24 * 3600_000);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function codeMap(): Record<string, Currency> {
|
||||
return Object.fromEntries(
|
||||
(Object.entries(COT_CODES) as [Currency, string][]).map(([ccy, code]) => [code, ccy])
|
||||
);
|
||||
}
|
||||
|
||||
function initByCcy<T>(): Record<string, Map<string, T>> {
|
||||
return Object.fromEntries(Object.keys(COT_CODES).map(k => [k, new Map<string, T>()]));
|
||||
}
|
||||
|
||||
function toWeeks(maps: Record<string, Map<string, CotWeek>>): Record<Currency, CotWeek[]> {
|
||||
const result = {} as Record<Currency, CotWeek[]>;
|
||||
for (const [ccy, m] of Object.entries(maps)) {
|
||||
result[ccy as Currency] = Array.from(m.values())
|
||||
.sort((a, b) => b.weekDate.localeCompare(a.weekDate))
|
||||
.slice(0, 26);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function int(v: string | undefined): number {
|
||||
const n = parseInt(v ?? "0", 10);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
// ── Parseurs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
interface TffRow {
|
||||
cftc_contract_market_code: string;
|
||||
report_date_as_yyyy_mm_dd: string;
|
||||
lev_money_positions_long: string;
|
||||
lev_money_positions_short: string;
|
||||
change_in_lev_money_long: string;
|
||||
change_in_lev_money_short: string;
|
||||
}
|
||||
|
||||
function parseTff(rows: TffRow[]): Record<Currency, CotWeek[]> {
|
||||
const codes = codeMap();
|
||||
const maps = initByCcy<CotWeek>();
|
||||
|
||||
for (const row of rows) {
|
||||
const ccy = codes[row.cftc_contract_market_code];
|
||||
if (!ccy) continue;
|
||||
const longs = int(row.lev_money_positions_long);
|
||||
const shorts = int(row.lev_money_positions_short);
|
||||
const total = longs + shorts;
|
||||
const dL = int(row.change_in_lev_money_long);
|
||||
const dS = int(row.change_in_lev_money_short);
|
||||
const weekDate = row.report_date_as_yyyy_mm_dd?.slice(0, 10);
|
||||
if (!weekDate) continue;
|
||||
maps[ccy].set(weekDate, {
|
||||
weekDate,
|
||||
net: longs - shorts,
|
||||
longPct: total > 0 ? Math.round(longs / total * 100) : 50,
|
||||
shortPct: total > 0 ? Math.round(shorts / total * 100) : 50,
|
||||
totalLev: total,
|
||||
deltaNet: dL - dS,
|
||||
deltaLong: dL,
|
||||
deltaShort: dS,
|
||||
});
|
||||
}
|
||||
return toWeeks(maps);
|
||||
}
|
||||
|
||||
interface LegacyRow {
|
||||
cftc_contract_market_code: string;
|
||||
report_date_as_yyyy_mm_dd: string;
|
||||
noncomm_positions_long_all: string;
|
||||
noncomm_positions_short_all:string;
|
||||
change_in_noncomm_long_all: string;
|
||||
change_in_noncomm_short_all:string;
|
||||
}
|
||||
|
||||
function parseLegacy(rows: LegacyRow[]): Record<Currency, CotWeek[]> {
|
||||
const codes = codeMap();
|
||||
const maps = initByCcy<CotWeek>();
|
||||
|
||||
for (const row of rows) {
|
||||
const ccy = codes[row.cftc_contract_market_code];
|
||||
if (!ccy) continue;
|
||||
const longs = int(row.noncomm_positions_long_all);
|
||||
const shorts = int(row.noncomm_positions_short_all);
|
||||
const total = longs + shorts;
|
||||
const dL = int(row.change_in_noncomm_long_all);
|
||||
const dS = int(row.change_in_noncomm_short_all);
|
||||
const weekDate = row.report_date_as_yyyy_mm_dd?.slice(0, 10);
|
||||
if (!weekDate) continue;
|
||||
maps[ccy].set(weekDate, {
|
||||
weekDate,
|
||||
net: longs - shorts,
|
||||
longPct: total > 0 ? Math.round(longs / total * 100) : 50,
|
||||
shortPct: total > 0 ? Math.round(shorts / total * 100) : 50,
|
||||
totalLev: total,
|
||||
deltaNet: dL - dS,
|
||||
deltaLong: dL,
|
||||
deltaShort: dS,
|
||||
});
|
||||
}
|
||||
return toWeeks(maps);
|
||||
}
|
||||
|
||||
// ── Route ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function sodaFetch(dataset: string): Promise<unknown[]> {
|
||||
const url = `${SODA_BASE}/${dataset}.json?$where=${encodeURIComponent(WHERE)}&$limit=${LIMIT}&$order=${encodeURIComponent(ORDER)}`;
|
||||
const res = await fetch(url, { cache: "no-store", headers: { "Accept": "application/json" } });
|
||||
if (!res.ok) throw new Error(`SODA ${dataset} failed: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
if (_cache && Date.now() - _cache.ts < cacheTtl()) {
|
||||
return NextResponse.json(_cache.data);
|
||||
}
|
||||
|
||||
try {
|
||||
const [tffRows, legacyRows] = await Promise.all([
|
||||
sodaFetch("gpe5-46if"), // TFF Futures Only
|
||||
sodaFetch("6dca-aqww"), // Legacy Futures Only
|
||||
]);
|
||||
|
||||
const data: CotHistory = {
|
||||
tff: parseTff(tffRows as TffRow[]),
|
||||
legacy: parseLegacy(legacyRows as LegacyRow[]),
|
||||
};
|
||||
|
||||
_cache = { data, ts: Date.now() };
|
||||
return NextResponse.json(data);
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: String(err) }, { status: 502 });
|
||||
}
|
||||
}
|
||||
+24
-6
@@ -31,21 +31,38 @@ const IDX_LEV_LONG = 14;
|
||||
const IDX_LEV_SHORT = 15;
|
||||
const IDX_DATE = 2;
|
||||
|
||||
// In-memory cache (1 semaine)
|
||||
let _cache: { data: Record<string, unknown>; ts: number } | null = null;
|
||||
const TTL = 7 * 24 * 3600_000;
|
||||
// In-memory cache — expire le vendredi suivant à 15h30 UTC (publication CFTC)
|
||||
// TTL max 4 jours pour garantir refresh chaque semaine
|
||||
let _cache: { data: Record<string, unknown>; ts: number; weekDate: string } | null = null;
|
||||
|
||||
function nextCftcRelease(): number {
|
||||
const now = new Date();
|
||||
const d = new Date(now);
|
||||
// Prochain vendredi 15:30 UTC
|
||||
const daysUntilFriday = (5 - d.getUTCDay() + 7) % 7 || 7;
|
||||
d.setUTCDate(d.getUTCDate() + daysUntilFriday);
|
||||
d.setUTCHours(15, 30, 0, 0);
|
||||
return d.getTime();
|
||||
}
|
||||
|
||||
function cacheTtl(): number {
|
||||
const ttlToRelease = nextCftcRelease() - Date.now();
|
||||
// max 4 jours pour éviter de bloquer sur de vieilles données
|
||||
return Math.min(ttlToRelease, 4 * 24 * 3600_000);
|
||||
}
|
||||
|
||||
export type { CotEntry } from "@/lib/types";
|
||||
|
||||
export async function GET() {
|
||||
if (_cache && Date.now() - _cache.ts < TTL) {
|
||||
if (_cache && Date.now() - _cache.ts < cacheTtl()) {
|
||||
return NextResponse.json(_cache.data);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(CFTC_URL, {
|
||||
next: { revalidate: 86400 * 7 },
|
||||
next: { revalidate: 86400 }, // revalidate quotidien — CFTC sort chaque vendredi
|
||||
headers: { "User-Agent": "Mozilla/5.0 (compatible; ForexDashboard/1.0)" },
|
||||
cache: "no-store", // forcer fetch frais pour l'in-memory cache ci-dessus
|
||||
});
|
||||
if (!res.ok) {
|
||||
return NextResponse.json({ error: `CFTC fetch failed: ${res.status}` }, { status: 502 });
|
||||
@@ -54,7 +71,8 @@ export async function GET() {
|
||||
const text = await res.text();
|
||||
const result = parseCOT(text);
|
||||
|
||||
_cache = { data: result, ts: Date.now() };
|
||||
const weekDate = Object.values(result as Record<string, CotEntry>)[0]?.weekDate ?? "";
|
||||
_cache = { data: result, ts: Date.now(), weekDate };
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: String(err) }, { status: 502 });
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// Calcule la performance hebdomadaire G10 en % vs USD
|
||||
// en comparant le vendredi de la semaine en cours au vendredi précédent
|
||||
|
||||
export interface FxWeeklyEntry {
|
||||
ccy: string;
|
||||
pct: number; // % change vs USD, positif = a apprécié
|
||||
current: number | null; // taux actuel (unités par USD)
|
||||
prev: number | null; // taux semaine précédente
|
||||
}
|
||||
|
||||
export interface FxWeeklyData {
|
||||
weekFrom: string; // "YYYY-MM-DD" — début de semaine (lundi)
|
||||
weekTo: string; // "YYYY-MM-DD" — fin de semaine (vendredi)
|
||||
prevFri: string; // "YYYY-MM-DD" — vendredi précédent
|
||||
currencies: FxWeeklyEntry[];
|
||||
}
|
||||
|
||||
const G10_CCYS = ["EUR","GBP","JPY","CHF","CAD","AUD","NZD"];
|
||||
// DXY basket weights pour USD
|
||||
const DXY_WEIGHTS: Record<string, number> = {
|
||||
EUR: 0.576, JPY: 0.136, GBP: 0.119, CAD: 0.091, SEK: 0.042, CHF: 0.036,
|
||||
};
|
||||
|
||||
let _cache: { data: FxWeeklyData; ts: number; key: string } | null = null;
|
||||
const TTL = 3600_000; // 1h
|
||||
|
||||
// Renvoie le vendredi de la semaine complétée la plus récente.
|
||||
// Dimanche → vendredi d'avant-hier (la semaine lun-ven vient de se terminer).
|
||||
// Lundi…jeudi → vendredi de la semaine précédente (la semaine courante n'est pas finie).
|
||||
// Vendredi → aujourd'hui. Samedi → hier.
|
||||
function lastFriday(from?: Date): Date {
|
||||
const d = from ? new Date(from) : new Date();
|
||||
const day = d.getDay(); // 0=dim … 6=sam
|
||||
const sub = (day - 5 + 7) % 7; // 0 si ven, 1 si sam, 2 si dim, 3 si lun, …, 6 si jeu
|
||||
d.setDate(d.getDate() - sub);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
function prevFridayFrom(fri: Date): Date {
|
||||
const d = new Date(fri);
|
||||
d.setDate(d.getDate() - 7);
|
||||
return d;
|
||||
}
|
||||
|
||||
function toISO(d: Date): string { return d.toISOString().slice(0, 10); }
|
||||
|
||||
async function fetchRates(date: string): Promise<Record<string, number> | null> {
|
||||
try {
|
||||
const ccys = [...G10_CCYS, "SEK"].join(",");
|
||||
const res = await fetch(
|
||||
`https://api.frankfurter.app/${date}?from=USD&to=${ccys}`,
|
||||
{ next: { revalidate: 3600 }, headers: { "Accept": "application/json" } }
|
||||
);
|
||||
if (!res.ok) return null;
|
||||
const json = await res.json() as { rates?: Record<string, number> };
|
||||
return json.rates ?? null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
// Optionnel : ?weekTo=YYYY-MM-DD pour forcer la semaine
|
||||
const forcedFriday = searchParams.get("weekTo");
|
||||
|
||||
const thisFri = forcedFriday ? new Date(forcedFriday) : lastFriday();
|
||||
const prevFri = prevFridayFrom(thisFri);
|
||||
const cacheKey = toISO(thisFri);
|
||||
|
||||
if (_cache && _cache.key === cacheKey && Date.now() - _cache.ts < TTL) {
|
||||
return NextResponse.json(_cache.data);
|
||||
}
|
||||
|
||||
const [currRates, prevRates] = await Promise.all([
|
||||
fetchRates(toISO(thisFri)),
|
||||
fetchRates(toISO(prevFri)),
|
||||
]);
|
||||
|
||||
if (!currRates || !prevRates) {
|
||||
return NextResponse.json({ error: "Frankfurter unavailable" }, { status: 502 });
|
||||
}
|
||||
|
||||
// Calcul % pour chaque devise G10
|
||||
const entries: FxWeeklyEntry[] = G10_CCYS.map(ccy => {
|
||||
const curr = currRates[ccy] ?? null;
|
||||
const prev = prevRates[ccy] ?? null;
|
||||
let pct = 0;
|
||||
if (curr && prev && prev > 0) {
|
||||
// rate = units of CCY per 1 USD
|
||||
// Si rate diminue → CCY apprécie → pct positif
|
||||
pct = ((prev - curr) / prev) * 100;
|
||||
}
|
||||
return { ccy, pct: Math.round(pct * 10) / 10, current: curr, prev };
|
||||
});
|
||||
|
||||
// USD : inverse pondéré du panier DXY
|
||||
let usdPct = 0;
|
||||
let totalWeight = 0;
|
||||
for (const [ccy, w] of Object.entries(DXY_WEIGHTS)) {
|
||||
const entry = entries.find(e => e.ccy === ccy);
|
||||
if (entry) { usdPct -= entry.pct * w; totalWeight += w; }
|
||||
}
|
||||
if (totalWeight > 0) usdPct /= totalWeight;
|
||||
entries.unshift({ ccy: "USD", pct: Math.round(usdPct * 10) / 10, current: 1, prev: 1 });
|
||||
|
||||
// Trier du plus fort au plus faible
|
||||
entries.sort((a, b) => b.pct - a.pct);
|
||||
|
||||
// Lundi de la semaine thisFri
|
||||
const monday = new Date(thisFri);
|
||||
monday.setDate(thisFri.getDate() - 4);
|
||||
|
||||
const data: FxWeeklyData = {
|
||||
weekFrom: toISO(monday),
|
||||
weekTo: toISO(thisFri),
|
||||
prevFri: toISO(prevFri),
|
||||
currencies: entries,
|
||||
};
|
||||
|
||||
_cache = { data, ts: Date.now(), key: cacheKey };
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
let body: {
|
||||
mode: "cb_analysis" | "expert_opinion" | "summary" | "divergence";
|
||||
mode: "cb_analysis" | "expert_opinion" | "summary" | "divergence" | "report_ccy";
|
||||
currency?: string;
|
||||
data?: unknown;
|
||||
userInput?: string;
|
||||
@@ -72,6 +72,28 @@ ${JSON.stringify(data, null, 2)}
|
||||
Explique en 3 phrases : pourquoi cette configuration est significative et quelle action de trading elle suggère.`;
|
||||
break;
|
||||
|
||||
case "report_ccy": {
|
||||
const d = data as {
|
||||
weeklyPct?: string; weekFrom?: string; weekTo?: string;
|
||||
cotNetTff?: number; cotDeltaTff?: number; cotLongPctTff?: number;
|
||||
cotNetLegacy?: number; cotDeltaLegacy?: number;
|
||||
yieldCurrent?: number; yieldDelta?: number;
|
||||
hint?: string;
|
||||
};
|
||||
const weekRange = d.weekFrom && d.weekTo ? `${d.weekFrom} au ${d.weekTo}` : "de la semaine";
|
||||
userMessage = `Rédige le paragraphe d'analyse hebdomadaire pour ${currency} (semaine du ${weekRange}).
|
||||
|
||||
Données disponibles :
|
||||
- Performance hebdomadaire vs USD : ${d.weeklyPct ?? "N/D"}
|
||||
- COT Hedge Funds (TFF) : net ${d.cotNetTff ?? "N/D"} contrats, variation semaine : ${d.cotDeltaTff != null ? (d.cotDeltaTff > 0 ? "+" : "") + d.cotDeltaTff : "N/D"}, % long : ${d.cotLongPctTff ?? "N/D"}%
|
||||
- COT Non-Commercial (Legacy) : net ${d.cotNetLegacy ?? "N/D"} contrats, variation : ${d.cotDeltaLegacy != null ? (d.cotDeltaLegacy > 0 ? "+" : "") + d.cotDeltaLegacy : "N/D"}
|
||||
- Rendement obligataire : ${d.yieldCurrent != null ? d.yieldCurrent + "%" : "N/D"} (Δ ${d.yieldDelta != null ? (d.yieldDelta > 0 ? "+" : "") + d.yieldDelta + "%" : "N/D"})
|
||||
${d.hint ? `- Contexte additionnel : ${d.hint}` : ""}
|
||||
|
||||
Rédige un paragraphe fluide de 80 à 120 mots, style analyste macro professionnel. Structure : (1) performance et catalyseurs de la semaine, (2) lecture institutionnelle (COT), (3) niveau ou seuil clé à surveiller. Pas de bullet points — texte continu.`;
|
||||
break;
|
||||
}
|
||||
|
||||
case "summary":
|
||||
default:
|
||||
userMessage = `Génère une synthèse macro hebdomadaire pour ${currency} basée sur ces données :
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { NewsItem } from "@/lib/newsfeed";
|
||||
export type { NewsItem } from "@/lib/newsfeed";
|
||||
|
||||
let _cache: { data: NewsItem[]; ts: number } | null = null;
|
||||
const TTL = 30 * 60_000; // 30 min
|
||||
const TTL = 5 * 60_000; // 5 min — actualités fraîches
|
||||
|
||||
export async function GET() {
|
||||
if (_cache && Date.now() - _cache.ts < TTL) {
|
||||
@@ -17,6 +17,6 @@ export async function GET() {
|
||||
|
||||
return NextResponse.json(
|
||||
{ items, fetchedAt: new Date().toISOString() },
|
||||
{ headers: { "Cache-Control": "s-maxage=1800, stale-while-revalidate=3600" } }
|
||||
{ headers: { "Cache-Control": "s-maxage=300, stale-while-revalidate=600" } }
|
||||
);
|
||||
}
|
||||
|
||||
+37
-5
@@ -12,8 +12,11 @@ import CalendarTab from "@/components/CalendarTab";
|
||||
import SentimentPairsTab from "@/components/SentimentPairsTab";
|
||||
import YieldsTab from "@/components/YieldsTab";
|
||||
import NewsTab from "@/components/NewsTab";
|
||||
import CotTab from "@/components/CotTab";
|
||||
import ReportTab from "@/components/ReportTab";
|
||||
import type { CalendarEvent } from "@/app/api/calendar/route";
|
||||
import type { NewsItem } from "@/app/api/news/route";
|
||||
import type { CotHistory } from "@/app/api/cot-history/route";
|
||||
|
||||
const REFRESH_MS = parseInt(process.env.NEXT_PUBLIC_REFRESH_INTERVAL_MS ?? "3600000");
|
||||
|
||||
@@ -25,10 +28,12 @@ export default function Dashboard() {
|
||||
const [cot, setCot] = useState<Record<string, CotEntry> | null>(null);
|
||||
const [calEvents, setCalEvents] = useState<CalendarEvent[]>([]);
|
||||
const [nextWeekAvail, setNextWeekAvail] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<"dashboard" | "calendar" | "pairs" | "yields" | "news">("dashboard");
|
||||
const [activeTab, setActiveTab] = useState<"dashboard" | "calendar" | "pairs" | "yields" | "news" | "cot" | "report">("dashboard");
|
||||
const [newsItems, setNewsItems] = useState<NewsItem[]>([]);
|
||||
const [newsLoading, setNewsLoading] = useState(false);
|
||||
const [rawSymbols, setRawSymbols] = useState<Array<{ name: string; longPercentage: number; shortPercentage: number; totalPositions: number }> | null>(null);
|
||||
const [cotHistory, setCotHistory] = useState<CotHistory | null>(null);
|
||||
const [cotLoading, setCotLoading] = useState(false);
|
||||
const [rawSymbols, setRawSymbols] = useState<Array<{ name: string; longPercentage: number; shortPercentage: number; longVolume: number; shortVolume: number; longPositions: number; shortPositions: number; totalPositions: number; avgLongPrice?: number; avgShortPrice?: number }> | null>(null);
|
||||
const [rateProbabilities, setRateProbabilities] = useState<RateProbData | null>(null);
|
||||
const [lastRefresh, setLastRefresh] = useState<Date>(new Date());
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -174,7 +179,7 @@ export default function Dashboard() {
|
||||
|
||||
// ── Sentiment Myfxbook ────────────────────────────────────────────────
|
||||
if (sentimentRes.status === "fulfilled" && !sentimentRes.value?.error && sentimentRes.value?.symbols) {
|
||||
const syms = sentimentRes.value.symbols as Array<{ name: string; longPercentage: number; shortPercentage: number; totalPositions: number }>;
|
||||
const syms = sentimentRes.value.symbols as Array<{ name: string; longPercentage: number; shortPercentage: number; longVolume: number; shortVolume: number; longPositions: number; shortPositions: number; totalPositions: number; avgLongPrice?: number; avgShortPrice?: number }>;
|
||||
setRawSymbols(syms);
|
||||
const mapped = parseSentimentSymbols(syms);
|
||||
setSentiment(mapped);
|
||||
@@ -233,6 +238,23 @@ export default function Dashboard() {
|
||||
if (activeTab === "news" && newsItems.length === 0) refreshNews();
|
||||
}, [activeTab, newsItems.length, refreshNews]);
|
||||
|
||||
const refreshCotHistory = useCallback(async () => {
|
||||
setCotLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/cot-history");
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
if (!json.error) setCotHistory(json as CotHistory);
|
||||
}
|
||||
} finally {
|
||||
setCotLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === "cot" && !cotHistory) refreshCotHistory();
|
||||
}, [activeTab, cotHistory, refreshCotHistory]);
|
||||
|
||||
const handleDivergenceUpdate = useCallback((currency: Currency, score: number) => {
|
||||
setActiveDivergences((prev) => {
|
||||
const filtered = prev.filter((d) => d.currency !== currency);
|
||||
@@ -292,7 +314,7 @@ export default function Dashboard() {
|
||||
|
||||
{/* Tab navigation */}
|
||||
<div className="flex gap-0 border-b border-slate-800 mb-4">
|
||||
{(["dashboard", "calendar", "pairs", "yields", "news"] as const).map((tab) => (
|
||||
{(["dashboard", "calendar", "pairs", "yields", "news", "cot", "report"] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
@@ -306,7 +328,9 @@ export default function Dashboard() {
|
||||
: tab === "calendar" ? "📅 Calendrier"
|
||||
: tab === "pairs" ? "↕ Paires"
|
||||
: tab === "yields" ? "📈 Yields 10Y"
|
||||
: "📰 Actualités"}
|
||||
: tab === "news" ? "📰 Actualités"
|
||||
: tab === "cot" ? "📊 COT"
|
||||
: "📋 Rapport"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -371,6 +395,14 @@ export default function Dashboard() {
|
||||
<NewsTab items={newsItems} loading={newsLoading} onRefresh={refreshNews} />
|
||||
)}
|
||||
|
||||
{activeTab === "cot" && (
|
||||
<CotTab history={cotHistory} loading={cotLoading} />
|
||||
)}
|
||||
|
||||
{activeTab === "report" && (
|
||||
<ReportTab calEvents={calEvents} drivers={drivers} cotHistory={cotHistory} />
|
||||
)}
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-4 pt-3 border-t border-slate-800 flex items-center gap-5 flex-wrap text-[10px] text-slate-600">
|
||||
<div className="flex items-center gap-1.5"><span className="w-2 h-2 rounded-full bg-emerald-500 shrink-0" /> Haussier / Sous-évalué</div>
|
||||
|
||||
Reference in New Issue
Block a user