26-source OSINT intelligence engine with live Jarvis dashboard, auto-refresh via SSE, optional LLM layer (4 providers), delta/memory system, and Telegram breaking news alerts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
202 lines
6.1 KiB
JavaScript
202 lines
6.1 KiB
JavaScript
// UN Comtrade — Global Trade Data
|
|
// Public preview endpoint requires no key. Full API needs free registration.
|
|
// Tracks commodity trade flows between nations: crude oil, gas, gold, semiconductors, arms.
|
|
// Reporter codes: 842 (US), 156 (China), 276 (Germany), 392 (Japan), 826 (UK), 643 (Russia), 356 (India)
|
|
|
|
import { safeFetch, daysAgo, today } from '../utils/fetch.mjs';
|
|
|
|
const BASE = 'https://comtradeapi.un.org/public/v1';
|
|
|
|
// Strategic commodity codes (HS classification)
|
|
const STRATEGIC_COMMODITIES = {
|
|
'2709': 'Crude Petroleum',
|
|
'2711': 'Natural Gas (LNG & Pipeline)',
|
|
'7108': 'Gold (unwrought/semi-manufactured)',
|
|
'8542': 'Semiconductors (Electronic Integrated Circuits)',
|
|
'93': 'Arms & Ammunition',
|
|
'2844': 'Radioactive Elements (Nuclear)',
|
|
'8471': 'Computers & Processing Units',
|
|
'2701': 'Coal',
|
|
'7601': 'Aluminium (unwrought)',
|
|
'2612': 'Uranium & Thorium Ores',
|
|
};
|
|
|
|
// Key reporter/partner country codes
|
|
const COUNTRIES = {
|
|
842: 'United States',
|
|
156: 'China',
|
|
276: 'Germany',
|
|
392: 'Japan',
|
|
826: 'United Kingdom',
|
|
643: 'Russia',
|
|
356: 'India',
|
|
410: 'South Korea',
|
|
158: 'Taiwan',
|
|
380: 'Italy',
|
|
};
|
|
|
|
// Get trade data for a specific reporter, commodity, and period
|
|
export async function getTradeData(opts = {}) {
|
|
const {
|
|
reporterCode = 842, // default: US
|
|
period = new Date().getFullYear(),
|
|
cmdCode = '2709', // default: crude oil
|
|
flowCode = 'M', // M = imports, X = exports
|
|
partnerCode = null, // null = all partners
|
|
} = opts;
|
|
|
|
const params = new URLSearchParams({
|
|
reporterCode: String(reporterCode),
|
|
period: String(period),
|
|
cmdCode,
|
|
flowCode,
|
|
});
|
|
if (partnerCode) params.set('partnerCode', String(partnerCode));
|
|
|
|
return safeFetch(`${BASE}/preview/C/A/HS?${params}`, { timeout: 20000 });
|
|
}
|
|
|
|
// Get bilateral trade between two countries for a commodity
|
|
export async function getBilateralTrade(reporter, partner, cmdCode, period) {
|
|
return getTradeData({
|
|
reporterCode: reporter,
|
|
partnerCode: partner,
|
|
cmdCode,
|
|
period: period || new Date().getFullYear(),
|
|
});
|
|
}
|
|
|
|
// Check multiple commodities for a given reporter
|
|
async function checkReporterCommodities(reporterCode, commodityCodes, period) {
|
|
const results = [];
|
|
for (const cmdCode of commodityCodes) {
|
|
const data = await getTradeData({
|
|
reporterCode,
|
|
cmdCode,
|
|
period,
|
|
flowCode: 'M', // imports
|
|
});
|
|
results.push({
|
|
commodity: STRATEGIC_COMMODITIES[cmdCode] || cmdCode,
|
|
cmdCode,
|
|
data,
|
|
});
|
|
}
|
|
return results;
|
|
}
|
|
|
|
// Compact a trade record for briefing output
|
|
function compactRecord(rec) {
|
|
return {
|
|
reporter: rec.reporterDesc || rec.reporterCode,
|
|
partner: rec.partnerDesc || rec.partnerCode,
|
|
commodity: rec.cmdDesc || rec.cmdCode,
|
|
flow: rec.flowDesc || rec.flowCode,
|
|
value: rec.primaryValue || rec.cifvalue || rec.fobvalue || null,
|
|
quantity: rec.qty || rec.netWgt || null,
|
|
unit: rec.qtyUnitAbbr || rec.qtyUnitDesc || null,
|
|
period: rec.period,
|
|
};
|
|
}
|
|
|
|
// Detect anomalies in trade data (unusually large flows, new partners, etc.)
|
|
function detectAnomalies(tradeRecords) {
|
|
const signals = [];
|
|
if (!Array.isArray(tradeRecords) || tradeRecords.length === 0) return signals;
|
|
|
|
const values = tradeRecords
|
|
.map(r => r.value)
|
|
.filter(v => typeof v === 'number' && v > 0);
|
|
|
|
if (values.length > 2) {
|
|
const avg = values.reduce((a, b) => a + b, 0) / values.length;
|
|
const stdDev = Math.sqrt(values.reduce((a, v) => a + (v - avg) ** 2, 0) / values.length);
|
|
|
|
tradeRecords.forEach(r => {
|
|
if (typeof r.value === 'number' && r.value > avg + 2 * stdDev) {
|
|
signals.push(
|
|
`OUTLIER: ${r.commodity} trade with ${r.partner} = $${(r.value / 1e9).toFixed(2)}B ` +
|
|
`(mean: $${(avg / 1e9).toFixed(2)}B)`
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
return signals;
|
|
}
|
|
|
|
// Briefing — check recent trade data for key commodities, detect anomalies
|
|
export async function briefing() {
|
|
const currentYear = new Date().getFullYear();
|
|
const prevYear = currentYear - 1;
|
|
|
|
// Key combinations to check: US imports of strategic commodities
|
|
const keyCommodities = ['2709', '2711', '7108', '8542', '93'];
|
|
const keyReporters = [842, 156]; // US, China
|
|
|
|
const tradeFlows = [];
|
|
const signals = [];
|
|
|
|
for (const reporter of keyReporters) {
|
|
for (const cmdCode of keyCommodities) {
|
|
// Try current year first, fall back to previous year
|
|
let data = await getTradeData({
|
|
reporterCode: reporter,
|
|
cmdCode,
|
|
period: currentYear,
|
|
flowCode: 'M',
|
|
});
|
|
|
|
// Comtrade returns data in different structures; normalize
|
|
let records = data?.data || data?.dataset || [];
|
|
if (!Array.isArray(records)) records = [];
|
|
|
|
// If no current year data, try previous year
|
|
if (records.length === 0) {
|
|
data = await getTradeData({
|
|
reporterCode: reporter,
|
|
cmdCode,
|
|
period: prevYear,
|
|
flowCode: 'M',
|
|
});
|
|
records = data?.data || data?.dataset || [];
|
|
if (!Array.isArray(records)) records = [];
|
|
}
|
|
|
|
const compact = records.slice(0, 10).map(compactRecord);
|
|
if (compact.length > 0) {
|
|
tradeFlows.push({
|
|
reporter: COUNTRIES[reporter] || reporter,
|
|
commodity: STRATEGIC_COMMODITIES[cmdCode] || cmdCode,
|
|
cmdCode,
|
|
topPartners: compact,
|
|
totalRecords: records.length,
|
|
});
|
|
|
|
// Run anomaly detection
|
|
const anomalies = detectAnomalies(compact);
|
|
signals.push(...anomalies);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
source: 'UN Comtrade',
|
|
timestamp: new Date().toISOString(),
|
|
tradeFlows,
|
|
signals: signals.length > 0
|
|
? signals
|
|
: ['No significant trade anomalies detected in sampled commodities'],
|
|
status: tradeFlows.length > 0 ? 'ok' : 'no_data',
|
|
note: 'Comtrade data often lags 1-2 months. Recent periods may be incomplete.',
|
|
coveredCommodities: STRATEGIC_COMMODITIES,
|
|
coveredCountries: COUNTRIES,
|
|
};
|
|
}
|
|
|
|
// Run standalone
|
|
if (process.argv[1]?.endsWith('comtrade.mjs')) {
|
|
const data = await briefing();
|
|
console.log(JSON.stringify(data, null, 2));
|
|
}
|