const frameContainer = document.querySelector("#frames"); const symbolSelect = document.querySelector("#symbol"); const signalModeSelect = document.querySelector("#signal-mode"); const customSymbol = document.querySelector("#custom-symbol"); const form = document.querySelector("#symbol-form"); const refreshButton = document.querySelector("#refresh"); const demoToggle = document.querySelector("#demo"); const runBacktestButton = document.querySelector("#run-backtest"); const runOptimizerButton = document.querySelector("#run-optimizer"); const runForecastButton = document.querySelector("#run-forecast"); const cancelOptimizerButton = document.querySelector("#cancel-optimizer"); const refreshHistoryButton = document.querySelector("#refresh-history"); const refreshOrdersButton = document.querySelector("#refresh-orders"); const paperOrderButton = document.querySelector("#paper-order"); const primaryPaperOrderButton = document.querySelector("#primary-paper-order"); let optimizerJobId = null; let optimizerPollTimer = null; let latestAnalysis = null; const statusColor = { STRONG_BUY: "green", BUY: "green", WEAK_BUY: "orange", STRONG_SELL: "red", SELL: "red", WEAK_SELL: "orange", NONE: "grey", }; const signalModeLabels = { balanced: "Ausgewogen", high_precision: "Hohe Trefferwahrscheinlichkeit", lux_style: "Lux-inspiriert", }; function money(value) { if (!value) return "--"; return Number(value).toLocaleString("de-CH", { maximumFractionDigits: value > 100 ? 2 : 5 }); } function compactNumber(value, suffix = "") { if (!Number.isFinite(value)) return "--"; const abs = Math.abs(value); const digits = abs >= 1000 ? 0 : abs >= 100 ? 1 : abs >= 10 ? 2 : 3; return `${Number(value).toLocaleString("de-CH", { maximumFractionDigits: digits })}${suffix}`; } function formatChartTime(timestamp) { if (!timestamp) return "--"; const millis = timestamp > 100000000000 ? timestamp : timestamp * 1000; return new Date(millis).toLocaleDateString("de-CH", { day: "2-digit", month: "2-digit" }); } function chartTicks(min, max, count = 5) { if (!Number.isFinite(min) || !Number.isFinite(max)) return []; if (min === max) return [min]; return Array.from({ length: count }, (_, index) => min + (index / (count - 1)) * (max - min)); } function renderChartFrame({ width, height, padLeft, padRight, padTop, padBottom, minY, maxY, y, xTicks = [], ySuffix = "", zeroY = null, splitX = null }) { const plotLeft = padLeft; const plotRight = width - padRight; const plotTop = padTop; const plotBottom = height - padBottom; const yTicks = chartTicks(minY, maxY, 5); const horizontal = yTicks.map(value => { const yy = y(value); return ` ${compactNumber(value, ySuffix)} `; }).join(""); const vertical = xTicks.map(tick => ` ${tick.label} `).join(""); const zero = Number.isFinite(zeroY) && zeroY >= plotTop && zeroY <= plotBottom ? `` : ""; const split = Number.isFinite(splitX) ? `` : ""; return ` ${horizontal} ${vertical} ${zero} ${split} `; } async function loadConfig() { const response = await fetch("/api/config"); const config = await response.json(); symbolSelect.innerHTML = ""; for (const symbol of config.available_symbols || []) { const option = document.createElement("option"); option.value = symbol; option.textContent = symbol; option.selected = symbol === config.symbol; symbolSelect.append(option); } signalModeSelect.innerHTML = ""; for (const mode of config.available_signal_modes || ["balanced", "high_precision", "lux_style"]) { const option = document.createElement("option"); option.value = mode; option.textContent = signalModeLabels[mode] || mode; option.selected = mode === (config.signal_mode || "balanced"); signalModeSelect.append(option); } } async function saveSettings(symbol, signalMode) { await fetch("/api/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ symbol, signal_mode: signalMode }), }); await loadConfig(); } async function refresh() { refreshButton.disabled = true; refreshButton.textContent = "Lädt..."; try { const demo = demoToggle.checked ? "?demo=1" : ""; const response = await fetch(`/api/analyze${demo}`); const data = await response.json(); latestAnalysis = data; render(data); loadHealth(); } finally { refreshButton.disabled = false; refreshButton.textContent = "Aktualisieren"; } } function render(data) { document.querySelector("#updated").textContent = `${data.symbol} · ${new Date(data.updated_at * 1000).toLocaleString("de-CH")}`; renderSignal(data.signal); renderFrames(data.frames, data.signal); renderPattern(data.pattern); renderCorrelations(data.correlations); renderMacro(data.macro); renderRiskPlan(data.risk_plan); renderSummaryChips(data); renderMethodology(data.methodology); renderIndicatorAudit(data.indicator_audit); renderDataQuality(data.data_quality); renderWarnings(data.warnings || []); } function signalLabel(type) { if (type === "STRONG_BUY") return "Kaufen"; if (type === "BUY") return "Kaufen"; if (type === "WEAK_BUY") return "Beobachten"; if (type === "STRONG_SELL") return "Short"; if (type === "SELL") return "Short"; if (type === "WEAK_SELL") return "Short beobachten"; return "Warten"; } function decisionCopy(signal, riskPlan) { if (!signal) return "Noch keine Daten geladen."; if (riskPlan?.paper_allowed) return "Signal ist stark genug fuer Paper-Trading. Risiko und Stop sind berechnet."; if (signal.signal_type === "WEAK_BUY") return "Setup entsteht, aber die Bestaetigung reicht noch nicht fuer eine Order."; if (signal.signal_type === "WEAK_SELL") return "Bearishes Setup entsteht, aber die Bestaetigung reicht noch nicht fuer eine Short-Order."; if (signal.signal_type === "NONE") return "Kein sauberer Einstieg. Das System wartet auf bessere Bestaetigung."; return "Signal vorhanden, aber der Risk-Plan blockiert die Ausfuehrung."; } function renderSummaryChips(data) { document.querySelector("#macro-chip").textContent = data.macro?.label || "--"; document.querySelector("#risk-chip").textContent = data.risk_plan?.paper_allowed ? "handelbar" : "blockiert"; document.querySelector("#data-chip").textContent = data.data_quality ? `${data.data_quality.mode}${data.data_quality.fallbacks ? `/${data.data_quality.fallbacks}` : ""}` : "--"; document.querySelector("#mode-chip").textContent = data.signal?.mode_label || "--"; } function renderRiskPlan(plan) { const target = document.querySelector("#risk-plan"); if (!target || !plan) return; const blockers = plan.blockers && plan.blockers.length ? plan.blockers.join(" · ") : "keine"; target.innerHTML = `
Richtung${plan.side || "--"}
Paper erlaubt${plan.paper_allowed ? "Ja" : "Nein"}
Menge${plan.quantity}
Notional${plan.notional}
Risiko${plan.risk_amount} (${plan.risk_per_trade_pct}%)
Modus${plan.mode}${plan.testnet ? " · Testnet" : ""}
Blocker${blockers}
`; paperOrderButton.disabled = !plan.paper_allowed; primaryPaperOrderButton.disabled = !plan.paper_allowed; } function renderSignal(signal) { const light = document.querySelector("#signal-light"); light.className = `signal-light ${statusColor[signal.signal_type] || "grey"}`; document.querySelector("#signal-type").textContent = signalLabel(signal.signal_type); document.querySelector("#decision-copy").textContent = decisionCopy(signal, latestAnalysis?.risk_plan); document.querySelector("#signal-meta").textContent = `${signal.mode_label || "Ausgewogen"} · ${signal.reasons.join(" · ")}`; document.querySelector("#entry-price").textContent = money(signal.entry_price); document.querySelector("#stop-loss").textContent = money(signal.stop_loss); document.querySelector("#target").textContent = money(signal.target); document.querySelector("#risk-reward").textContent = signal.risk_reward ? signal.risk_reward.toFixed(2) : "--"; const blockers = signal.blockers || []; document.querySelector("#signal-blockers").innerHTML = blockers.length ? blockers.map(blocker => `${blocker}`).join("") : `Keine Signalblocker`; renderDecisionGraphic(signal); } function renderDecisionGraphic(signal) { const target = document.querySelector("#decision-graphic"); if (!target) return; const params = signal.params || {}; const strong = Number(params.strong_buy || 7); const buy = Number(params.buy || 5); const weak = Number(params.weak_buy || 3); const maxScore = Math.max(strong + 1, signal.strength || 0, 1); const scorePct = Math.max(0, Math.min(100, ((signal.strength || 0) / maxScore) * 100)); const rr = Number(signal.risk_reward || 0); const rrTarget = Number(params.rr_good || 1.4); const rrPct = Math.max(0, Math.min(100, (rr / Math.max(rrTarget * 1.8, 1)) * 100)); const confidencePct = Math.max(0, Math.min(100, Number(signal.confidence || 0))); const thresholds = [ { label: "W", title: "Weak", value: weak }, { label: "B", title: "Buy/Sell", value: buy }, { label: "S", title: "Strong", value: strong }, ].map(item => `${item.label}`).join(""); target.innerHTML = `
Signalgrafik ${signal.side || "--"} · ${signal.signal_type}
${thresholds}
${signal.strength ?? 0}
${rr ? rr.toFixed(2) : "--"}
${signal.confidence ?? 0}%
`; } function renderFrames(frames, signal) { frameContainer.innerHTML = ""; const timeframeScores = {}; const weightedFrames = [["15m", 1], ["30m", 1], ["4h", 2], ["1d", 2]]; const scoredFrames = weightedFrames.filter(([name]) => frames[name]).map(([name]) => name); (signal?.score_parts || []).forEach((part, index) => { timeframeScores[scoredFrames[index]] = part; }); for (const [timeframe, frame] of Object.entries(frames)) { const article = document.createElement("article"); article.className = "timeframe"; const statuses = Object.values(frame.statuses || {}); const green = statuses.filter(status => status.status === "green").length; const orange = statuses.filter(status => status.status === "orange").length; const red = statuses.filter(status => status.status === "red").length; const total = Math.max(statuses.length, 1); const score = timeframeScores[timeframe] || {}; article.innerHTML = `

${timeframe} ${money(frame.price)}

${score.points ?? 0} ${score.label || "neutral"}
${statuses.map(status => ``).join("")}
`; for (const [name, status] of Object.entries(frame.statuses)) { const row = document.createElement("div"); row.className = "status-row"; row.innerHTML = `
${name}${status.message}
`; article.append(row); } frameContainer.append(article); } } function renderPattern(pattern) { const target = document.querySelector("#pattern"); const rows = [ ["Status", pattern.pattern_detected ? pattern.pattern_type : "Kein W-Muster"], ["Konfidenz", `${pattern.confidence || 0}%`], ["Entry", money(pattern.optimal_entry)], ["Stop", money(pattern.stop_loss)], ["Ziel", money(pattern.target_price)], ["R/R", pattern.risk_reward ? pattern.risk_reward.toFixed(2) : "--"], ]; target.innerHTML = rows.map(([label, value]) => `
${label}${value}
`).join(""); } function renderCorrelations(rows) { const target = document.querySelector("#correlations"); if (!rows.length) { target.textContent = "Keine Korrelationsdaten"; return; } target.innerHTML = rows.map(row => { const corrClass = row.correlation >= 0 ? "text-green" : "text-red"; const strengthClass = row.relative_strength >= 0 ? "text-green" : "text-red"; return `
${row.symbol} ${row.correlation ?? "--"} ${row.relative_strength > 0 ? "+" : ""}${row.relative_strength}%
`; }).join(""); } function renderWarnings(warnings) { document.querySelector("#warnings").innerHTML = warnings.map(warning => `
${warning}
`).join(""); } function renderDataQuality(dataQuality) { const updated = document.querySelector("#updated"); if (!dataQuality) return; const marker = dataQuality.mode === "live" && dataQuality.fallbacks === 0 ? "Live-Daten" : `${dataQuality.mode} · ${dataQuality.fallbacks} Fallbacks`; updated.textContent = `${updated.textContent} · ${marker}`; } function renderMethodology(methodology) { const target = document.querySelector("#methodology"); if (!methodology) { target.textContent = "Keine Methodikdaten"; return; } target.innerHTML = `
Modell

${methodology.model_type}

${methodology.accuracy_status}

Formel
Eingebaut

${methodology.macro_status}

Fehlt noch
`; } function renderIndicatorAudit(audit) { const target = document.querySelector("#indicator-audit"); if (!target || !audit) return; const checks = (audit.checks || []).map(check => `
${check.name} ${check.tradingview} ${check.method} ${check.note}
`).join(""); const values = Object.entries(audit.values || {}).map(([timeframe, row]) => `
${timeframe} RSI ${row.rsi14 ?? "--"} MACD ${row.macd ?? "--"} / ${row.macd_signal ?? "--"} Hist ${row.macd_hist ?? "--"} EMA20 ${money(row.ema20)} EMA50 ${money(row.ema50)} SMA200 ${money(row.sma200)}
`).join(""); target.innerHTML = `

${audit.source}

${audit.feed_warning}

${checks}
${values}
`; } function renderMacro(macro) { const target = document.querySelector("#macro"); if (!macro) { target.textContent = "Keine Makrodaten"; return; } const scoreClass = macro.status === "green" ? "text-green" : macro.status === "red" ? "text-red" : "text-orange"; const components = (macro.components || []).map(component => `
${component.name} ${component.message}
`).join(""); target.innerHTML = `
${macro.label} Score: ${macro.score} ${macro.source_note || ""}
${components}
`; } async function runBacktest() { runBacktestButton.disabled = true; runBacktestButton.textContent = "Backtest läuft..."; const target = document.querySelector("#backtest"); target.innerHTML = `

Historische Binance-Kerzen werden geladen und Strategien verglichen.

`; try { const demo = demoToggle.checked ? "?demo=1" : ""; const response = await fetch(`/api/backtest${demo}`); const data = await response.json(); renderBacktest(data); } finally { runBacktestButton.disabled = false; runBacktestButton.textContent = "Backtest starten"; } } async function runOptimizer() { runOptimizerButton.disabled = true; cancelOptimizerButton.disabled = false; runOptimizerButton.textContent = "Optimiert..."; const target = document.querySelector("#backtest"); const progressTarget = document.querySelector("#optimizer-progress"); target.innerHTML = `

Parameter-Kombinationen werden per Backtest, Out-of-sample und Walk-forward verglichen.

`; progressTarget.innerHTML = `
`; try { const response = await fetch("/api/optimize/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ demo: demoToggle.checked }), }); const data = await response.json(); optimizerJobId = data.job_id; pollOptimizer(); } catch (error) { target.innerHTML = `

Optimizer konnte nicht gestartet werden: ${error}

`; runOptimizerButton.disabled = false; cancelOptimizerButton.disabled = true; runOptimizerButton.textContent = "Live optimieren"; } } async function pollOptimizer() { if (!optimizerJobId) return; const response = await fetch(`/api/optimize/status/${optimizerJobId}`); const data = await response.json(); renderOptimizerProgress(data); if (data.status === "running") { optimizerPollTimer = window.setTimeout(pollOptimizer, 1200); return; } runOptimizerButton.disabled = false; cancelOptimizerButton.disabled = true; runOptimizerButton.textContent = "Live optimieren"; optimizerJobId = null; if (data.status === "done" || data.status === "cancelled") { renderOptimizer(data.result || { best: data.best, candidates: data.candidates, settings: {} }); } else if (data.status === "error") { document.querySelector("#backtest").innerHTML = `

Optimizer-Fehler: ${data.error}

`; } } function renderOptimizerProgress(data) { const target = document.querySelector("#optimizer-progress"); const total = data.total || 1; const pct = Math.round((data.done || 0) / total * 100); const rows = (data.candidates || []).slice(0, 5); target.innerHTML = `
${data.done || 0}/${data.total || "?"} · ${pct}% Status: ${data.status}
${rows.map(row => `
Score ${row.score} ${row.trades} Trades · PF ${row.avg_profit_factor} · OOS ${row.out_of_sample_score} · WF ${row.walk_forward_score}
`).join("")}
${renderConvergenceChart(data.best_history || [])} `; } async function loadForecast() { const target = document.querySelector("#forecast"); runForecastButton.disabled = true; runForecastButton.textContent = "Lädt..."; target.innerHTML = `

Forecast wird aus den letzten 4h-Kerzen berechnet.

`; try { const demo = demoToggle.checked ? "?demo=1" : ""; const response = await fetch(`/api/forecast${demo}`); const data = await response.json(); renderForecast(data); } finally { runForecastButton.disabled = false; runForecastButton.textContent = "Forecast laden"; } } function renderForecast(data) { const target = document.querySelector("#forecast"); if (data.error) { target.innerHTML = `

${data.error}

`; return; } target.innerHTML = `
${data.settings.mode} ${data.settings.timeframe} ${data.settings.candles} Kerzen Horizont ${data.settings.horizon} Vol ${data.metrics.volatility_pct}%
${renderForecastChart(data)} ${renderLearningCurve(data.learning_curve || [])} `; } function renderBacktest(data) { const target = document.querySelector("#backtest"); const rows = data.summary || []; if (!rows.length) { target.innerHTML = `

Keine Trades im Backtest gefunden.

`; return; } target.innerHTML = `
${data.settings.mode} ${data.settings.candles} Kerzen Horizont ${data.settings.horizon_candles} x 4h Backtest nutzt echte 4h/1d-Historie
SymbolModusTradesWinrateReturnPFDD
${rows.map(row => `
${row.symbol} ${signalModeLabels[row.mode] || row.mode} ${row.trades} ${row.win_rate}% ${row.total_return_pct}% ${row.profit_factor} ${row.max_drawdown_pct}%
`).join("")}
${renderPriceChart(rows[0]?.chart || {})} ${renderEquityChart(rows[0]?.equity_curve || [])} `; } function renderOptimizer(data) { const target = document.querySelector("#backtest"); if (!data.best) { target.innerHTML = `

Keine optimierbaren Trades gefunden.

`; return; } const params = data.best.params; const quality = data.best.quality || { passed: false, flags: ["Quality-Daten fehlen"] }; const bestRun = (data.best.best_runs || [])[0] || {}; const applyDisabled = quality.passed ? "" : "disabled"; target.innerHTML = `
${data.settings.mode} Score ${data.best.score} Train ${data.best.train_score} OOS ${data.best.out_of_sample_score} WF ${data.best.walk_forward_score} ${data.best.trades} Trades PF ${data.best.avg_profit_factor} Winrate ${data.best.avg_win_rate}% Return ${data.best.total_return_pct}% DD ${data.best.max_drawdown_pct}%
${quality.passed ? "Quality-Gate bestanden" : "Quality-Gate blockiert"} ${quality.flags && quality.flags.length ? quality.flags.join(" · ") : "keine Warnungen"}
Vorschlag

weak=${params.weak_buy}, buy=${params.buy}, strong=${params.strong_buy}, R/R=${params.rr_good}/${params.rr_excellent}, RSI=${params.rsi_oversold}/${params.rsi_bullish}/${params.rsi_overbought}

${renderConvergenceChart(data.convergence || [])} ${renderPriceChart(bestRun.chart || {})} ${renderEquityChart(bestRun.equity_curve || [])} `; const applyButton = document.querySelector("#apply-optimizer"); if (!quality.passed) return; applyButton.addEventListener("click", async () => { await fetch("/api/optimize/apply", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ signal_params: params, symbol_params: data.best.symbol_params || {} }), }); await refresh(); }); } function renderForecastChart(data) { const history = data.history || []; const forecast = data.forecast || []; if (!history.length || !forecast.length) return ""; const width = 760; const height = 330; const padLeft = 74; const padRight = 24; const padTop = 20; const padBottom = 42; const allValues = [ ...history.flatMap(point => [point.high, point.low]), ...forecast.flatMap(point => [point.upper, point.lower, point.price]), ]; const min = Math.min(...allValues); const max = Math.max(...allValues); const total = history.length + forecast.length; const x = index => padLeft + index * ((width - padLeft - padRight) / Math.max(total - 1, 1)); const y = value => height - padBottom - ((value - min) / Math.max(max - min, 1)) * (height - padTop - padBottom); const histLine = history.map((point, index) => `${index ? "L" : "M"}${x(index).toFixed(1)} ${y(point.close).toFixed(1)}`).join(" "); const start = history.length - 1; const forecastLine = forecast.map((point, index) => `${index ? "L" : "M"}${x(start + index + 1).toFixed(1)} ${y(point.price).toFixed(1)}`).join(" "); const upper = forecast.map((point, index) => `${index ? "L" : "M"}${x(start + index + 1).toFixed(1)} ${y(point.upper).toFixed(1)}`).join(" "); const lower = forecast.map((point, index) => `L${x(start + forecast.length - index).toFixed(1)} ${y(forecast[forecast.length - 1 - index].lower).toFixed(1)}`).join(" "); const splitX = x(start); const firstTime = history[0]?.time; const lastHistoryTime = history[history.length - 1]?.time; const forecastLabel = `+${forecast[forecast.length - 1].step} Kerzen`; const frame = renderChartFrame({ width, height, padLeft, padRight, padTop, padBottom, minY: min, maxY: max, y, xTicks: [ { x: x(0), label: formatChartTime(firstTime), anchor: "start" }, { x: splitX, label: formatChartTime(lastHistoryTime) }, { x: x(total - 1), label: forecastLabel, anchor: "end" }, ], splitX, }); return `
Forecast-Pfad ${frame} Forecast
HistorieForecastBand = Unsicherheit
`; } function renderLearningCurve(points) { if (!points.length) return ""; const series = points.map(point => ({ x: point.window, y: point.error_pct })); return renderLineCard("Lernkurve: Fehler sinkt mit mehr Historie", series, "window", "error_pct", true); } function renderConvergenceChart(points) { if (!points.length) return ""; const series = points.map(point => ({ x: point.step, y: point.best_score })); return renderLineCard("Optimizer-Konvergenz: bester Score", series, "step", "best_score", false); } function renderLineCard(title, points, xLabel, yLabel, invertGood) { const width = 720; const height = 250; const padLeft = 66; const padRight = 22; const padTop = 20; const padBottom = 42; const xs = points.map(point => point.x); const ys = points.map(point => point.y); const minX = Math.min(...xs); const maxX = Math.max(...xs); const minY = Math.min(...ys); const maxY = Math.max(...ys); const x = value => padLeft + ((value - minX) / Math.max(maxX - minX, 1)) * (width - padLeft - padRight); const y = value => height - padBottom - ((value - minY) / Math.max(maxY - minY, 1)) * (height - padTop - padBottom); const line = points.map((point, index) => `${index ? "L" : "M"}${x(point.x).toFixed(1)} ${y(point.y).toFixed(1)}`).join(" "); const last = points[points.length - 1]; const frame = renderChartFrame({ width, height, padLeft, padRight, padTop, padBottom, minY, maxY, y, ySuffix: yLabel.includes("pct") ? "%" : "", zeroY: minY < 0 && maxY > 0 ? y(0) : null, xTicks: [ { x: x(minX), label: compactNumber(minX), anchor: "start" }, { x: x((minX + maxX) / 2), label: compactNumber((minX + maxX) / 2) }, { x: x(maxX), label: compactNumber(maxX), anchor: "end" }, ], }); return `
${title} ${frame} ${yLabel} ${xLabel}
${xLabel}: ${last.x}${yLabel}: ${last.y}
`; } async function loadHistory() { const response = await fetch("/api/history?limit=12"); const data = await response.json(); const target = document.querySelector("#history"); target.innerHTML = (data.runs || []).map(run => { const payload = run.payload || {}; const best = payload.best || (payload.summary || [])[0] || {}; return `
#${run.id} ${run.kind} ${new Date(run.created_at * 1000).toLocaleString("de-CH")} ${run.label || "--"} Trades ${best.trades ?? "--"} · Return ${best.total_return_pct ?? "--"} · Score ${best.score ?? "--"}
`; }).join("") || `

Keine gespeicherten Runs.

`; } async function loadHealth() { const response = await fetch("/api/health"); const data = await response.json(); const target = document.querySelector("#health"); if (!target) return; target.innerHTML = `
Status${data.status}
Execution${data.execution.mode} · Kill ${data.execution.kill_switch ? "an" : "aus"}
Exchange${data.exchange_guard.live_ready ? "live bereit" : data.exchange_guard.blockers.join(" · ")}
Risk/Trade${data.risk_management.risk_per_trade_pct}%
Optimizer${data.optimizer_jobs.running} laufend · ${data.optimizer_jobs.total} total
`; } function renderPriceChart(chart) { const candles = chart.candles || []; if (!candles.length) return ""; const trades = chart.trades || []; const width = 720; const height = 300; const padLeft = 72; const padRight = 22; const padTop = 20; const padBottom = 42; const lows = candles.map(candle => candle.low); const highs = candles.map(candle => candle.high); const min = Math.min(...lows); const max = Math.max(...highs); const xStep = (width - padLeft - padRight) / Math.max(candles.length, 1); const y = value => height - padBottom - ((value - min) / Math.max(max - min, 1)) * (height - padTop - padBottom); const candleSvg = candles.map((candle, index) => { const x = padLeft + index * xStep + xStep / 2; const color = candle.close >= candle.open ? "up" : "down"; const bodyTop = y(Math.max(candle.open, candle.close)); const bodyHeight = Math.max(2, Math.abs(y(candle.open) - y(candle.close))); return ` `; }).join(""); const firstTime = candles[0].time || 0; const lastTime = candles[candles.length - 1].time || firstTime + 1; const timeX = time => padLeft + ((time - firstTime) / Math.max(lastTime - firstTime, 1)) * (width - padLeft - padRight); const tradeSvg = trades.map(trade => { const x = timeX(trade.entry_time || firstTime); return ` `; }).join(""); const frame = renderChartFrame({ width, height, padLeft, padRight, padTop, padBottom, minY: min, maxY: max, y, xTicks: [ { x: timeX(firstTime), label: formatChartTime(firstTime), anchor: "start" }, { x: timeX((firstTime + lastTime) / 2), label: formatChartTime((firstTime + lastTime) / 2) }, { x: timeX(lastTime), label: formatChartTime(lastTime), anchor: "end" }, ], }); return `
Preis / Trades ${frame} ${candleSvg} ${tradeSvg} Preis
gruen: Aufwaertskerzerot: Abwaertskerze/Exit
`; } async function loadPaperOrders() { const response = await fetch("/api/paper/orders"); const data = await response.json(); const target = document.querySelector("#paper-orders"); target.innerHTML = (data.orders || []).map(order => `
#${order.id} ${order.symbol} ${order.side} · ${order.status} Menge ${order.quantity} Entry ${money(order.entry)} · Stop ${money(order.stop)} · Ziel ${money(order.target)}
`).join("") || `

Keine Paper Orders.

`; } async function createPaperOrder() { if (!latestAnalysis) return; const response = await fetch("/api/paper/order", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ signal: latestAnalysis.signal }), }); if (!response.ok) { const data = await response.json(); document.querySelector("#paper-orders").innerHTML = `

${data.error}: ${(data.risk_plan?.blockers || []).join(" · ")}

`; return; } await loadPaperOrders(); } function renderEquityChart(points) { if (!points.length) return ""; const width = 720; const height = 250; const padLeft = 66; const padRight = 22; const padTop = 20; const padBottom = 42; const values = points.map(point => point.equity_pct); const dds = points.map(point => -Math.abs(point.drawdown_pct || 0)); const min = Math.min(...values, ...dds, -1); const max = Math.max(...values, 1); const x = index => padLeft + index * ((width - padLeft - padRight) / Math.max(points.length - 1, 1)); const y = value => height - padBottom - ((value - min) / Math.max(max - min, 1)) * (height - padTop - padBottom); const equityLine = points.map((point, index) => `${index ? "L" : "M"}${x(index).toFixed(1)} ${y(point.equity_pct).toFixed(1)}`).join(" "); const drawdownLine = points.map((point, index) => `${index ? "L" : "M"}${x(index).toFixed(1)} ${y(-Math.abs(point.drawdown_pct || 0)).toFixed(1)}`).join(" "); const frame = renderChartFrame({ width, height, padLeft, padRight, padTop, padBottom, minY: min, maxY: max, y, ySuffix: "%", zeroY: y(0), xTicks: [ { x: x(0), label: "Start", anchor: "start" }, { x: x(Math.floor((points.length - 1) / 2)), label: "Mitte" }, { x: x(points.length - 1), label: "Ende", anchor: "end" }, ], }); return `
Equity / Drawdown ${frame} %
Equity %Drawdown %
`; } function activateTab(name) { document.querySelectorAll(".tab-button").forEach(button => { button.classList.toggle("active", button.dataset.tab === name); }); document.querySelectorAll(".tab-panel").forEach(panel => { panel.classList.toggle("active", panel.id === `tab-${name}`); }); if (name === "optimizer") loadHistory(); if (name === "forecast") loadForecast(); if (name === "risk") { loadHealth(); loadPaperOrders(); } } form.addEventListener("submit", async event => { event.preventDefault(); const symbol = (customSymbol.value || symbolSelect.value).trim().toUpperCase(); const signalMode = signalModeSelect.value; if (!symbol) return; await saveSettings(symbol, signalMode); customSymbol.value = ""; await refresh(); }); refreshButton.addEventListener("click", refresh); runBacktestButton.addEventListener("click", runBacktest); runOptimizerButton.addEventListener("click", runOptimizer); runForecastButton.addEventListener("click", loadForecast); refreshHistoryButton.addEventListener("click", loadHistory); refreshOrdersButton.addEventListener("click", loadPaperOrders); paperOrderButton.addEventListener("click", createPaperOrder); primaryPaperOrderButton.addEventListener("click", createPaperOrder); cancelOptimizerButton.addEventListener("click", async () => { if (!optimizerJobId) return; await fetch(`/api/optimize/cancel/${optimizerJobId}`, { method: "POST" }); cancelOptimizerButton.disabled = true; }); demoToggle.addEventListener("change", refresh); signalModeSelect.addEventListener("change", async () => { await saveSettings(symbolSelect.value, signalModeSelect.value); await refresh(); }); document.querySelectorAll(".tab-button").forEach(button => { button.addEventListener("click", () => activateTab(button.dataset.tab)); }); loadConfig().then(refresh);