import { config } from './config.js'; // Definierte handelbare Assets. emoji + sector nur fuer die UI. const ASSETS = [ { symbol: 'AAPL', name: 'Apple', emoji: '🍎', sector: 'Tech', type: 'stock', fallback: 195 }, { symbol: 'TSLA', name: 'Tesla', emoji: '🚗', sector: 'Auto', type: 'stock', fallback: 250 }, { symbol: 'NVDA', name: 'Nvidia', emoji: '🎮', sector: 'Chips', type: 'stock', fallback: 120 }, { symbol: 'AMZN', name: 'Amazon', emoji: 'đŸ“Ļ', sector: 'Retail', type: 'stock', fallback: 185 }, { symbol: 'MSFT', name: 'Microsoft', emoji: 'đŸĒŸ', sector: 'Tech', type: 'stock', fallback: 420 }, { symbol: 'GOOGL', name: 'Alphabet', emoji: '🔍', sector: 'Tech', type: 'stock', fallback: 175 }, { symbol: 'META', name: 'Meta', emoji: '👓', sector: 'Social', type: 'stock', fallback: 500 }, { symbol: 'AMD', name: 'AMD', emoji: '⚡', sector: 'Chips', type: 'stock', fallback: 160 }, { symbol: 'NFLX', name: 'Netflix', emoji: 'đŸŽŦ', sector: 'Media', type: 'stock', fallback: 650 }, { symbol: 'GME', name: 'GameStop', emoji: 'đŸ•šī¸', sector: 'Meme', type: 'stock', fallback: 25 }, // Krypto + Meme (simuliert, hohe Volatilitaet) { symbol: 'BTC', name: 'Bitcoin', emoji: 'â‚ŋ', sector: 'Krypto', type: 'crypto', fallback: 67000, vol: 2.2 }, { symbol: 'ETH', name: 'Ethereum', emoji: '💎', sector: 'Krypto', type: 'crypto', fallback: 3500, vol: 2.4 }, { symbol: 'DOGE', name: 'Dogecoin', emoji: '🐕', sector: 'Meme', type: 'crypto', fallback: 0.16, vol: 4 }, { symbol: 'BULL', name: 'PlayBull Coin',emoji: '🐂', sector: 'Haus', type: 'crypto', fallback: 1.00, vol: 5 }, ]; const HISTORY_LEN = 120; /** @type {Map} */ const market = new Map(); let io = null; function rnd(min, max) { return min + Math.random() * (max - min); } function initAsset(a) { const price = a.fallback; return { symbol: a.symbol, name: a.name, emoji: a.emoji, sector: a.sector, type: a.type, price, prevClose: price, open: price, high: price, low: price, change: 0, changePct: 0, momentum: 0, // treibt die naechsten Ticks (Manipulation haengt sich hier rein) baseVol: a.vol || (a.type === 'crypto' ? 2 : 1), // Grund-Volatilitaet in % halted: false, history: Array.from({ length: HISTORY_LEN }, () => ({ t: Date.now(), p: price })), }; } async function fetchRealPrice(symbol) { if (!config.FINNHUB_API_KEY) return null; try { const url = `https://finnhub.io/api/v1/quote?symbol=${encodeURIComponent(symbol)}&token=${config.FINNHUB_API_KEY}`; const res = await fetch(url, { signal: AbortSignal.timeout(6000) }); if (!res.ok) return null; const j = await res.json(); // c = current, pc = previous close if (j && typeof j.c === 'number' && j.c > 0) { return { price: j.c, prevClose: j.pc > 0 ? j.pc : j.c, high: j.h || j.c, low: j.l || j.c, open: j.o || j.c }; } } catch { /* ignore -> fallback */ } return null; } export async function initMarket(socketServer) { io = socketServer; for (const a of ASSETS) market.set(a.symbol, initAsset(a)); // Echte Startkurse fuer Aktien von Finnhub laden (Krypto bleibt simuliert) const stockSymbols = ASSETS.filter(a => a.type === 'stock').map(a => a.symbol); await Promise.all(stockSymbols.map(async (sym) => { const real = await fetchRealPrice(sym); if (real) { const m = market.get(sym); m.price = real.price; m.prevClose = real.prevClose; m.open = real.open; m.high = real.high; m.low = real.low; m.history = Array.from({ length: HISTORY_LEN }, () => ({ t: Date.now(), p: real.price })); recalc(m); } })); // Live-Tick alle 1s setInterval(tick, 1000); return market; } function recalc(m) { m.change = m.price - m.prevClose; m.changePct = m.prevClose ? (m.change / m.prevClose) * 100 : 0; if (m.price > m.high) m.high = m.price; if (m.price < m.low) m.low = m.price; } function tick() { const payload = []; for (const m of market.values()) { if (!m.halted) { // Random walk + Momentum (Manipulation) + leichte Rueckkehr zur Mitte const noise = rnd(-1, 1) * m.baseVol * 0.4; // % pro Tick const drift = m.momentum; // % pro Tick aus Manipulation const pctMove = (noise + drift) / 100; m.price = Math.max(0.0001, m.price * (1 + pctMove)); m.momentum *= 0.85; // Momentum klingt ab if (Math.abs(m.momentum) < 0.0005) m.momentum = 0; recalc(m); m.history.push({ t: Date.now(), p: m.price }); if (m.history.length > HISTORY_LEN) m.history.shift(); } payload.push({ s: m.symbol, p: round(m.price), c: round(m.change), cp: round(m.changePct, 2), h: round(m.halted ? 1 : 0), }); } if (io) io.emit('prices', { t: Date.now(), data: payload }); } function round(n, d = 4) { const f = Math.pow(10, d); return Math.round(n * f) / f; } // ---- Oeffentliche API ---- export function getAsset(symbol) { return market.get(symbol); } export function getPrice(symbol) { const m = market.get(symbol); return m ? m.price : null; } export function listAssets() { return [...market.values()].map(m => ({ symbol: m.symbol, name: m.name, emoji: m.emoji, sector: m.sector, type: m.type, price: round(m.price), change: round(m.change), changePct: round(m.changePct, 2), high: round(m.high), low: round(m.low), prevClose: round(m.prevClose), halted: m.halted, })); } export function getHistory(symbol) { const m = market.get(symbol); if (!m) return []; return m.history.map(h => ({ t: h.t, p: round(h.p) })); } /** * Globale Markt-Manipulation. Wirkt fuer ALLE Nutzer (geteilter Live-Status). * action: 'pump' | 'dump' | 'set' | 'halt' | 'resume' | 'reset' */ export function manipulate(symbol, action, value, byUser = 'Jemand') { const m = market.get(symbol); if (!m) return { ok: false, error: 'Unbekanntes Symbol' }; switch (action) { case 'pump': m.momentum += (value && value > 0 ? value : 2); // % Boost pro Tick break; case 'dump': m.momentum -= (value && value > 0 ? value : 2); break; case 'set': { const v = Number(value); if (!(v > 0)) return { ok: false, error: 'Ungueltiger Preis' }; m.price = v; recalc(m); m.history.push({ t: Date.now(), p: m.price }); break; } case 'halt': m.halted = true; break; case 'resume': m.halted = false; break; case 'reset': m.momentum = 0; m.halted = false; m.price = m.prevClose; recalc(m); break; default: return { ok: false, error: 'Unbekannte Aktion' }; } if (io) io.emit('manip', { symbol, action, by: byUser, price: round(m.price) }); return { ok: true, symbol, price: round(m.price), momentum: m.momentum, halted: m.halted }; } export { ASSETS };