"use strict"; // ── Live-Anbindung an server.py via WebSocket (Fallback: Polling) ────────── const $ = (id) => document.getElementById(id); function fmt(v, d = 3) { return (v === null || v === undefined || isNaN(v)) ? "—" : Number(v).toFixed(d); } function curSym(m) { const c = m && m.currency; return c === "EUR" ? "€" : c === "USD" ? "$" : (c ? c + " " : "$"); } // ── Akustischer CLOSE-Alarm ───────────────────────────────────────────────── // Web-Audio (kein Sound-File). Mobile entsperrt Audio erst nach der ersten // Nutzer-Interaktion → wir starten/reaktivieren den Context bei jedem Tap. let _audioCtx = null, _lastBeep = 0; function _unlockAudio() { try { if (!_audioCtx) _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); if (_audioCtx.state === "suspended") _audioCtx.resume(); } catch (e) {} } window.addEventListener("pointerdown", _unlockAudio); window.addEventListener("touchstart", _unlockAudio); // Globaler Ton-Schalter (Mute-Button oben rechts; überlebt Reloads via localStorage) let _muted = localStorage.getItem("oil_muted") === "1"; function _tone(freq, startOffset, dur) { if (_muted) return; const o = _audioCtx.createOscillator(), g = _audioCtx.createGain(); o.type = "square"; o.frequency.value = freq; o.connect(g); g.connect(_audioCtx.destination); const t = _audioCtx.currentTime + startOffset; g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(0.35, t + 0.02); g.gain.exponentialRampToValueAtTime(0.0001, t + dur); o.start(t); o.stop(t + dur + 0.03); } function beepCloseAlert() { if (_muted) return; if (!_audioCtx || _audioCtx.state !== "running") return; // erst nach 1. Tap aktiv const now = Date.now(); if (now - _lastBeep < 4000) return; // max. alle 4 s _lastBeep = now; _tone(880, 0, 0.18); // Zwei-Ton-Alarm (hoch→tief) _tone(620, 0.22, 0.20); } // Ton bei Empfehlung: LONG = aufsteigend, SHORT = absteigend (vom Close-Alarm // unterscheidbar). Sofort beim Signal-Wechsel + WIEDERHOLUNG alle 30 s, solange // der Button blinkt (Empfehlung aktiv & flat) — einmaliger Piep war überhörbar. const _REC_REBEEP_MS = 30000; let _lastRecBeep = 0, _lastRecSignal = null, _audioHintShown = false, _lastSrCloseCount = null; function beepRecommendation(sig, isNew) { if (_muted) return; // auch kein Sperr-Toast if (!_audioCtx || _audioCtx.state !== "running") { // Audio noch nicht freigeschaltet (Mobile braucht 1× Tippen) → 1× erklären if (isNew && !_audioHintShown) { _audioHintShown = true; toast("🔇 Ton gesperrt — einmal auf die Seite tippen aktiviert ihn", false); } return; } const now = Date.now(); if (now - _lastRecBeep < (isNew ? 3000 : _REC_REBEEP_MS)) return; _lastRecBeep = now; if (sig === "LONG") { _tone(520, 0, 0.16); _tone(780, 0.18, 0.20); } // ↑ else { _tone(780, 0, 0.16); _tone(520, 0.18, 0.20); } // ↓ } // ── Gesamtempfehlung: Konfidenz-Ring + Bias-Balken + Modul-Chips ─────────── function renderVerdict(v) { const sig = v.headline || "—"; const cls = sig === "LONG" ? "long" : sig === "SHORT" ? "short" : ""; const arr = sig === "LONG" ? "▲" : sig === "SHORT" ? "▼" : "◌"; $("vd-signal").className = "vd-signal " + cls; // Zeitebene, für die die Empfehlung gilt (M1/M5/M15/M30/H1) $("vd-signal").textContent = `${arr} ${sig}` + (v.tf ? ` · ${v.tf}` : ""); // Konfidenz-Ring (conic-gradient bis conf%, Farbe nach Richtung) const conf = v.conf || 0; const col = sig === "LONG" ? "var(--up)" : sig === "SHORT" ? "var(--down)" : "var(--dim)"; const deg = Math.max(0, Math.min(100, conf)) * 3.6; $("vd-ring").style.background = `conic-gradient(${col} ${deg}deg, #21262d ${deg}deg)`; $("vd-conf").textContent = conf ? conf + "%" : "—"; // Konsens-Text + Bias-Nadel (-1..+1 → 0..100 %) const total = v.total || 0, agree = v.agree || 0; const bias = typeof v.bias === "number" ? v.bias : 0; const strength = Math.abs(bias) >= 0.5 ? "stark" : Math.abs(bias) >= 0.2 ? "leicht" : "gemischt"; const dir = bias > 0.05 ? "LONG" : bias < -0.05 ? "SHORT" : "neutral"; $("vd-consensus").textContent = sig === "—" ? "keine Daten" : `Modul-Konsens: ${strength} ${dir} · ${agree}/${total} Module für ${sig}`; $("vd-needle").style.left = ((bias + 1) / 2 * 100).toFixed(1) + "%"; // Modul-Chips const vote = (x) => x > 0 ? ["long", "▲"] : x < 0 ? ["short", "▼"] : ["flat", "—"]; $("vd-modules").innerHTML = (v.votes || []).map(mod => { const [c, a] = vote(mod.vote); return `