Initial PlayBull release with Trilogy Hub integration.

Includes homelab deploy tooling, image position/zoom manifest persistence, and full trading/casino stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-02 08:31:36 +02:00
commit 94b6cc3be9
60 changed files with 9206 additions and 0 deletions

184
server/market.js Normal file
View File

@@ -0,0 +1,184 @@
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<string, any>} */
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 };