import { Users, Holdings, Trades } from './db.js'; import { getAsset } from './market.js'; // Position-Netting: kombiniert bestehende Position mit neuem Trade. function applyPosition(q0, a0, delta, price) { if (q0 === 0 || Math.sign(q0) === Math.sign(delta)) { const newQ = q0 + delta; const newA = (Math.abs(q0) * a0 + Math.abs(delta) * price) / (Math.abs(q0) + Math.abs(delta)); return { q: newQ, a: newA }; } // Gegenrichtung if (Math.abs(delta) < Math.abs(q0)) return { q: q0 + delta, a: a0 }; if (Math.abs(delta) === Math.abs(q0)) return { q: 0, a: 0 }; return { q: q0 + delta, a: price }; // Flip } /** * side: 'buy' | 'sell'. qty > 0. 'sell' kann Short eroeffnen. */ export function executeTrade(userId, symbol, side, qty) { const asset = getAsset(symbol); if (!asset) return { ok: false, error: 'Unbekanntes Symbol' }; if (asset.halted) return { ok: false, error: `${symbol} ist pausiert (halted)` }; qty = Number(qty); if (!(qty > 0)) return { ok: false, error: 'Menge muss > 0 sein' }; const user = Users.byId(userId); if (!user) return { ok: false, error: 'User nicht gefunden' }; const price = asset.price; const notional = qty * price; const delta = side === 'buy' ? qty : -qty; const h = Holdings.get(userId, symbol) || { quantity: 0, avg_price: 0 }; let newBalance; if (side === 'buy') { if (user.balance < notional) return { ok: false, error: 'Nicht genug Guthaben' }; newBalance = user.balance - notional; } else { // Verkauf bringt Cash. Short-Eroeffnung erlaubt, aber Sicherheit pruefen. const result = applyPosition(h.quantity, h.avg_price, delta, price); if (result.q < 0 && user.balance < Math.abs(result.q) * price * 0.5) { return { ok: false, error: 'Nicht genug Sicherheit fuer Short (50% Margin)' }; } newBalance = user.balance + notional; } const pos = applyPosition(h.quantity, h.avg_price, delta, price); Users.setBalance(userId, newBalance); if (Math.abs(pos.q) < 1e-9) Holdings.remove(userId, symbol); else Holdings.upsert(userId, symbol, pos.q, pos.a); Trades.add(userId, symbol, side, qty, price); return { ok: true, symbol, side, qty, price: round(price), balance: round(newBalance), position: { quantity: round(pos.q, 6), avgPrice: round(pos.a) }, }; } export function getPortfolio(userId) { const user = Users.byId(userId); if (!user) return null; const holdings = Holdings.forUser(userId).map(h => { const a = getAsset(h.symbol); const price = a ? a.price : h.avg_price; const value = h.quantity * price; const cost = h.quantity * h.avg_price; const pnl = value - cost; // funktioniert fuer Long & Short const pnlPct = cost !== 0 ? (pnl / Math.abs(cost)) * 100 : 0; return { symbol: h.symbol, name: a?.name || h.symbol, emoji: a?.emoji || '', quantity: round(h.quantity, 6), avgPrice: round(h.avg_price), price: round(price), value: round(value), pnl: round(pnl), pnlPct: round(pnlPct, 2), side: h.quantity >= 0 ? 'long' : 'short', }; }); const holdingsValue = holdings.reduce((s, h) => s + h.value, 0); return { balance: round(user.balance), holdingsValue: round(holdingsValue), netWorth: round(user.balance + holdingsValue), holdings, }; } function round(n, d = 2) { const f = Math.pow(10, d); return Math.round(n * f) / f; }