Files
forex-dashboard/app/api/cot-history/route.ts
T
Capucine Gest 1a2ea02b22 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>
2026-06-08 14:33:23 +02:00

175 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });
}
}