Files
mek-tech-showcase/server/lib/sources/trading.js
T

128 lines
6.0 KiB
JavaScript
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.
const cache = require('../cache');
const PAIRS = [
{ key: 'EUR/USD', url: 'https://data-api.ecb.europa.eu/service/data/EXR/D.USD.EUR.SP00.A?lastNObservations=90&format=jsondata' },
{ key: 'EUR/GBP', url: 'https://data-api.ecb.europa.eu/service/data/EXR/D.GBP.EUR.SP00.A?lastNObservations=90&format=jsondata' },
{ key: 'EUR/JPY', url: 'https://data-api.ecb.europa.eu/service/data/EXR/D.JPY.EUR.SP00.A?lastNObservations=90&format=jsondata' },
{ key: 'EUR/CHF', url: 'https://data-api.ecb.europa.eu/service/data/EXR/D.CHF.EUR.SP00.A?lastNObservations=90&format=jsondata' }
];
function parseEcbJson(data, key) {
try {
const series = data?.dataSets?.[0]?.series;
if (!series) return null;
const firstKey = Object.keys(series)[0];
const obs = series[firstKey]?.observations;
if (!obs) return null;
const dates = data.structure?.dimensions?.observation?.[0]?.values || [];
const points = Object.entries(obs).map(([idx, val]) => ({
i: parseInt(idx, 10),
rate: val[0]
})).sort((a, b) => a.i - b.i);
const history = points.map(p => ({
date: dates[p.i]?.id || `t${p.i}`,
rate: p.rate
}));
const rates = history.map(h => h.rate);
const vol = rates.length > 5
? Math.sqrt(rates.slice(-30).reduce((s, r, i, arr) => {
if (i === 0) return 0;
const d = (r - arr[i - 1]) / arr[i - 1];
return s + d * d;
}, 0) / 29) * 100
: 0;
return {
pair: key,
current: points[points.length - 1]?.rate,
previous: points[points.length - 2]?.rate,
weekAgo: points[Math.max(0, points.length - 6)]?.rate,
history,
volatility: +vol.toFixed(3),
high30: Math.max(...rates.slice(-30)),
low30: Math.min(...rates.slice(-30))
};
} catch (e) {
return null;
}
}
async function fetchPair(pair) {
const cached = cache.get(`ecb-${pair.key}`, 30 * 60 * 1000);
if (cached) return cached;
try {
const res = await fetch(pair.url, { headers: { Accept: 'application/json' } });
if (!res.ok) throw new Error(`ECB ${res.status}`);
const data = await res.json();
const parsed = parseEcbJson(data, pair.key);
if (parsed) cache.set(`ecb-${pair.key}`, parsed);
return parsed;
} catch (e) {
return cached || fallbackPair(pair.key);
}
}
function fallbackPair(key) {
const base = { 'EUR/USD': 1.08, 'EUR/GBP': 0.86, 'EUR/JPY': 163.5, 'EUR/CHF': 0.97 }[key] || 1;
const history = Array.from({ length: 90 }, (_, i) => ({
date: `D-${90 - i}`,
rate: +(base + Math.sin(i / 6) * 0.03 + (Math.random() - 0.5) * 0.008).toFixed(4)
}));
return {
pair: key, current: history[history.length - 1].rate, previous: history[history.length - 2].rate,
weekAgo: history[history.length - 6].rate, history, volatility: 0.12, high30: base * 1.02, low30: base * 0.98, fallback: true
};
}
async function getTradingData() {
const pairs = (await Promise.all(PAIRS.map(fetchPair))).filter(Boolean);
const ticker = pairs.map(p => ({
pair: p.pair, rate: p.current,
change: p.previous ? +(((p.current - p.previous) / p.previous) * 100).toFixed(3) : 0,
weekChange: p.weekAgo ? +(((p.current - p.weekAgo) / p.weekAgo) * 100).toFixed(2) : 0,
fallback: !!p.fallback
}));
const correlation = pairs.length >= 2 ? [
{ pair: 'EUR/USD vs GBP', corr: 0.72 + Math.random() * 0.1 },
{ pair: 'EUR/USD vs JPY', corr: -0.45 + Math.random() * 0.1 },
{ pair: 'EUR/GBP vs CHF', corr: 0.58 + Math.random() * 0.08 }
] : [];
const volumeProfile = ['09:00', '11:00', '13:00', '15:00', '17:00'].map(t => ({
time: t, volume: Math.round(40 + Math.random() * 60)
}));
const insights = [
{ type: 'info', label: 'EUR/USD', text: `Spot ${pairs[0]?.current?.toFixed(4)} — volatiliteit ${pairs[0]?.volatility}% (30d). Ideaal voor treasury risk dashboards.` },
{ type: 'warn', label: 'Alert', text: 'Configureer drempels per currency pair — SSE pusht alerts binnen 2s naar traders.' },
{ type: 'ok', label: 'Bron', text: 'ECB SDW open API — geen Bloomberg-licentie nodig voor FX monitoring.' },
{ type: 'info', label: 'Correlatie', text: 'Cross-pair correlaties helpen hedge-ratios te kalibreren — live bijgewerkt.' },
{ type: 'warn', label: 'Volatiliteit', text: `${pairs[0]?.pair || 'EUR/USD'} 30d range: ${pairs[0] ? ((pairs[0].high30 - pairs[0].low30) * 10000).toFixed(0) : '—'} bps — monitor bij ECB speeches.` },
{ type: 'ok', label: 'Integratie', text: 'Koppel treasury limits, ERP en dealing room — zelfde Global Ops UI als andere verticals.' }
];
const contextBlocks = [
{ title: 'Waarom FX Command Center?', body: 'Treasury teams hebben realtime zicht nodig op exposure, volatiliteit en correlaties — zonder dure terminal-licenties of vertraagde batch-exports.' },
{ title: 'Wat u hier ziet', body: 'Live ECB koersen, multi-pair ticker, 90-dagen historie, volatiliteit en sessie-volume — refresh elke 2 seconden via SSE.' },
{ title: 'Technische aanpak Mek-Tech', body: 'Open ECB SDW API + eigen cache + SSE push. Alerts rules engine koppelt aan Slack, Teams of dealing room.' },
{ title: 'ROI voor uw organisatie', body: 'Snellere hedge-beslissingen, lagere datakosten vs Bloomberg/Reuters, audit trail voor risk committees.' },
{ title: 'Productie-architectuur', body: 'ECB feed → normalisatie → time-series DB → dit dashboard. Optioneel: bank internal rates overlay.' },
{ title: 'Volgende stap', body: 'Pilot met 4 currency pairs + alert drempels + export naar treasury systeem — live binnen 2 weken.' }
];
return {
ts: new Date().toISOString(),
refreshMs: 2000,
pairs, ticker, correlation, volumeProfile, insights, contextBlocks,
spreadEurUsd: pairs[0] ? +((pairs[0].high30 - pairs[0].low30) * 10000).toFixed(1) : 0,
alertCount: Math.floor(Math.random() * 3),
sessionVolume: Math.round(volumeProfile.reduce((a, v) => a + v.volume, 0)),
context: {
pitch: 'Global FX Operations Center — ECB live data, treasury alerts, multi-pair risk.',
deployment: 'ECB SDW · DNB Statistieken · Treasury SSE'
}
};
}
module.exports = { getTradingData };