"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; // ── Laufzeit-Uhr des offenen Trades ─────────────────────────────────────────── // Öffnungszeit kommt als echte Epoch (Broker-Offset serverseitig korrigiert) im // Snapshot (`position.open_time`); der Ticker läuft eigenständig jede Sekunde, // damit die Uhr auch zwischen den Snapshots (4 s Poll / 1 s WS) flüssig zählt. let _tradeOpenTime = 0; function _fmtDuration(sec) { sec = Math.max(0, Math.floor(sec)); const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60; const p2 = (n) => String(n).padStart(2, "0"); return h > 0 ? `${h}:${p2(m)}:${p2(s)}` : `${m}:${p2(s)}`; } function _tickTradeDuration() { const el = document.getElementById("tb-dur"), hd = document.getElementById("tb-runtime"); if (!el) return; if (!_tradeOpenTime) { el.textContent = "—"; if (hd) hd.textContent = ""; return; } const sec = Date.now() / 1000 - _tradeOpenTime; const txt = _fmtDuration(sec); el.textContent = txt; if (hd) hd.textContent = "⏱ " + txt; // Ab 2 h dezent hervorheben (Time-Stop greift bei 120 min in Phase „Init") el.className = "tb-val" + (sec >= 7200 ? " warn" : ""); } setInterval(_tickTradeDuration, 1000); 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, snap) { 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"; // Live-Kosten-Chip (Spread/ATR — gemessen Ø 0,265; Nacht 0,32–0,50 = Falle): // reine Info, die die abgeschaltete Dead-Hours-Sperre ehrlich ersetzt. const cr = snap && snap.cost_ratio; let costTxt = ""; if (cr != null) { const tag = cr < 0.20 ? "✅ günstig" : cr <= 0.32 ? "· normal" : "⚠ teuer"; costTxt = ` · Kosten ${fmt(cr, 2)}×ATR ${tag}`; } $("vd-consensus").textContent = (sig === "—" ? "keine Daten" : `Modul-Konsens: ${strength} ${dir} · ${agree}/${total} Module für ${sig}`) + costTxt; $("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 `