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}
${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
${methodology.signal_formula.map(item => `- ${item}
`).join("")}
Eingebaut
${methodology.macro_status}
${(methodology.included_inputs || []).map(item => `- ${item}
`).join("")}
Fehlt noch
${methodology.missing_inputs.map(item => `- ${item}
`).join("")}
`;
}
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
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}
${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
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
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);