Add permanent single live demo toggle to dashboard

Этот коммит содержится в:
Виктор
2026-05-08 04:19:32 +09:00
родитель cee28f478c
Коммит b4ff7ef804
+6 -5
Просмотреть файл
@@ -9,7 +9,7 @@
:root { --bg:#07101f; --sidebar:#0b1428; --card:#111c36; --line:#243659; --text:#edf3ff; --muted:#91a2c8; --accent:#60a5fa; --good:#22c55e; --bad:#ef4444; --warn:#f59e0b; } :root { --bg:#07101f; --sidebar:#0b1428; --card:#111c36; --line:#243659; --text:#edf3ff; --muted:#91a2c8; --accent:#60a5fa; --good:#22c55e; --bad:#ef4444; --warn:#f59e0b; }
*{box-sizing:border-box} body{margin:0;font-family:Inter,Arial,sans-serif;background:var(--bg);color:var(--text)} *{box-sizing:border-box} body{margin:0;font-family:Inter,Arial,sans-serif;background:var(--bg);color:var(--text)}
.app{display:grid;grid-template-columns:240px 1fr;min-height:100vh}.side{background:linear-gradient(180deg,#0b1428,#09101f);border-right:1px solid var(--line);padding:18px;position:sticky;top:0;height:100vh}.brand{font-weight:900;font-size:22px;margin-bottom:6px}.sub{color:var(--muted);font-size:12px;margin-bottom:20px}.nav button{width:100%;margin:5px 0;text-align:left;background:transparent;border:1px solid transparent;color:var(--muted);padding:12px;border-radius:12px}.nav button.active,.nav button:hover{background:#12203e;color:white;border-color:#2b4270}.main{padding:18px}.top{display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:16px}.topbar{display:flex;gap:10px;flex-wrap:wrap;align-items:center} .app{display:grid;grid-template-columns:240px 1fr;min-height:100vh}.side{background:linear-gradient(180deg,#0b1428,#09101f);border-right:1px solid var(--line);padding:18px;position:sticky;top:0;height:100vh}.brand{font-weight:900;font-size:22px;margin-bottom:6px}.sub{color:var(--muted);font-size:12px;margin-bottom:20px}.nav button{width:100%;margin:5px 0;text-align:left;background:transparent;border:1px solid transparent;color:var(--muted);padding:12px;border-radius:12px}.nav button.active,.nav button:hover{background:#12203e;color:white;border-color:#2b4270}.main{padding:18px}.top{display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:16px}.topbar{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
select,input,button{padding:10px 12px;border-radius:10px;border:1px solid #31466e;background:#0d1830;color:var(--text);font-weight:650}button{cursor:pointer}.primary{background:linear-gradient(135deg,#2563eb,#7c3aed);border-color:#7aa2ff}.good{border-color:#166534;color:#bbf7d0}.bad{border-color:#7f1d1d;color:#fecaca}.warn{border-color:#92400e;color:#fde68a}.card{background:rgba(17,28,54,.96);border:1px solid var(--line);border-radius:16px;padding:15px;box-shadow:0 16px 32px rgba(0,0,0,.22)}.cards{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:14px}.label{color:var(--muted);font-size:12px;margin-bottom:7px}.value{font-size:22px;font-weight:900}.pill{display:inline-block;padding:6px 10px;border-radius:999px;background:#16223f;border:1px solid #30466f;color:#bdd0ff;font-size:13px}.grid{display:grid;grid-template-columns:1fr 400px;gap:14px}.section{display:none}.section.active{display:block}#chart{height:610px}.box{background:#0b1429;border:1px solid #22375f;border-radius:13px;padding:12px;margin:8px 0 14px}.line{display:flex;justify-content:space-between;gap:12px;padding:7px 0;border-bottom:1px solid rgba(145,162,214,.12)}.line:last-child{border-bottom:0}.line b{color:#fff}.row{display:flex;gap:8px;flex-wrap:wrap;margin:9px 0}.table{width:100%;border-collapse:collapse}.table th,.table td{padding:10px;border-bottom:1px solid #22375f;text-align:left;font-size:13px}.table th{color:var(--muted)}.muted{color:var(--muted);font-size:13px}.text{color:#c8d5fa;font-size:13px;line-height:1.45}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}.three{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.small{font-size:12px}.success{color:var(--good)}.dangerText{color:var(--bad)} select,input,button{padding:10px 12px;border-radius:10px;border:1px solid #31466e;background:#0d1830;color:var(--text);font-weight:650}button{cursor:pointer}.primary{background:linear-gradient(135deg,#2563eb,#7c3aed);border-color:#7aa2ff}.good{border-color:#166534;color:#bbf7d0}.bad{border-color:#7f1d1d;color:#fecaca}.warn{border-color:#92400e;color:#fde68a}.card{background:rgba(17,28,54,.96);border:1px solid var(--line);border-radius:16px;padding:15px;box-shadow:0 16px 32px rgba(0,0,0,.22)}.cards{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:14px}.label{color:var(--muted);font-size:12px;margin-bottom:7px}.value{font-size:22px;font-weight:900}.pill{display:inline-block;padding:6px 10px;border-radius:999px;background:#16223f;border:1px solid #30466f;color:#bdd0ff;font-size:13px}.modeSwitch{display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid #31466e;border-radius:999px;background:#0d1830}.modeSwitch span{font-size:13px;color:var(--muted);font-weight:800}.switch{position:relative;display:inline-block;width:58px;height:30px}.switch input{display:none}.slider{position:absolute;cursor:pointer;inset:0;background:#14532d;border-radius:999px;transition:.2s;border:1px solid #166534}.slider:before{position:absolute;content:"";height:22px;width:22px;left:4px;top:3px;background:white;border-radius:50%;transition:.2s}.switch input:checked+.slider{background:#7f1d1d;border-color:#ef4444}.switch input:checked+.slider:before{transform:translateX(27px)}.grid{display:grid;grid-template-columns:1fr 400px;gap:14px}.section{display:none}.section.active{display:block}#chart{height:610px}.box{background:#0b1429;border:1px solid #22375f;border-radius:13px;padding:12px;margin:8px 0 14px}.line{display:flex;justify-content:space-between;gap:12px;padding:7px 0;border-bottom:1px solid rgba(145,162,214,.12)}.line:last-child{border-bottom:0}.line b{color:#fff}.row{display:flex;gap:8px;flex-wrap:wrap;margin:9px 0}.table{width:100%;border-collapse:collapse}.table th,.table td{padding:10px;border-bottom:1px solid #22375f;text-align:left;font-size:13px}.table th{color:var(--muted)}.muted{color:var(--muted);font-size:13px}.text{color:#c8d5fa;font-size:13px;line-height:1.45}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}.three{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.small{font-size:12px}.success{color:var(--good)}.dangerText{color:var(--bad)}
@media(max-width:1100px){.app{grid-template-columns:1fr}.side{position:relative;height:auto}.cards{grid-template-columns:1fr 1fr}.grid,.two,.three{grid-template-columns:1fr}}@media(max-width:650px){.cards{grid-template-columns:1fr}} @media(max-width:1100px){.app{grid-template-columns:1fr}.side{position:relative;height:auto}.cards{grid-template-columns:1fr 1fr}.grid,.two,.three{grid-template-columns:1fr}}@media(max-width:650px){.cards{grid-template-columns:1fr}}
</style> </style>
</head> </head>
@@ -30,7 +30,7 @@
<main class="main"> <main class="main">
<div class="top"> <div class="top">
<div><div class="value">Панель торгового бота</div><div class="muted">мониторинг сигналов, PNL, лимиты, CoinEx, Demo/Live</div></div> <div><div class="value">Панель торгового бота</div><div class="muted">мониторинг сигналов, PNL, лимиты, CoinEx, Demo/Live</div></div>
<div class="topbar"><select id="marketSelect"></select><button onclick="loadSelectedMarket()">Открыть пару</button><button class="primary" onclick="autoPickMarket()">Автовыбор пары</button><span class="pill" id="status">загрузка...</span></div> <div class="topbar"><div class="modeSwitch"><span>DEMO</span><label class="switch"><input id="liveToggle" type="checkbox" onchange="toggleLiveMode(this.checked)"><span class="slider"></span></label><span>LIVE</span></div><select id="marketSelect"></select><button onclick="loadSelectedMarket()">Открыть пару</button><button class="primary" onclick="autoPickMarket()">Автовыбор пары</button><span class="pill" id="status">загрузка...</span></div>
</div> </div>
<section id="dashboard" class="section active"> <section id="dashboard" class="section active">
@@ -69,7 +69,7 @@
<div><div class="label">Мин. оценка AI</div><input id="limitScore" type="number" value="65"></div> <div><div class="label">Мин. оценка AI</div><input id="limitScore" type="number" value="65"></div>
<div><div class="label">Макс. открытых позиций</div><input id="limitPositions" type="number" value="3"></div> <div><div class="label">Макс. открытых позиций</div><input id="limitPositions" type="number" value="3"></div>
</div> </div>
<div class="row"><button class="good" onclick="setMode('demo')">Demo счет</button><button class="bad" onclick="setMode('live')">Live счет</button><button class="primary" onclick="saveLimits()">Сохранить лимиты</button></div> <div class="row"><div class="modeSwitch"><span>DEMO</span><label class="switch"><input id="liveToggleLimits" type="checkbox" onchange="toggleLiveMode(this.checked)"><span class="slider"></span></label><span>LIVE</span></div><button class="primary" onclick="saveLimits()">Сохранить лимиты</button></div>
<div id="limitBox" class="box text">Текущие лимиты будут показаны здесь.</div> <div id="limitBox" class="box text">Текущие лимиты будут показаны здесь.</div>
</div> </div>
</section> </section>
@@ -92,7 +92,8 @@ const chartEl=document.getElementById('chart');const chart=LightweightCharts.cre
function $(id){return document.getElementById(id)} function esc(s){return String(s??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]))} function getMarket(){return $('marketSelect').value||'BTCUSDT'} function actionRu(a){return{buy:'Покупать',sell:'Продавать',hold:'Ждать'}[a]||a||'—'} function $(id){return document.getElementById(id)} function esc(s){return String(s??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]))} function getMarket(){return $('marketSelect').value||'BTCUSDT'} function actionRu(a){return{buy:'Покупать',sell:'Продавать',hold:'Ждать'}[a]||a||'—'}
function showTab(id,btn){document.querySelectorAll('.section').forEach(x=>x.classList.remove('active'));$(id).classList.add('active');document.querySelectorAll('.nav button').forEach(x=>x.classList.remove('active'));btn.classList.add('active');if(id==='trade')loadTrades();if(['strategies','coinex','limits'].includes(id))loadLegacy();setTimeout(()=>chart.applyOptions({width:chartEl.clientWidth,height:chartEl.clientHeight||610}),50)} function showTab(id,btn){document.querySelectorAll('.section').forEach(x=>x.classList.remove('active'));$(id).classList.add('active');document.querySelectorAll('.nav button').forEach(x=>x.classList.remove('active'));btn.classList.add('active');if(id==='trade')loadTrades();if(['strategies','coinex','limits'].includes(id))loadLegacy();setTimeout(()=>chart.applyOptions({width:chartEl.clientWidth,height:chartEl.clientHeight||610}),50)}
function renderDecision(obj){const d=obj.decision||obj,ind=d.indicators||obj.indicators||{};$('aiResult').innerHTML=`<div class="line"><span>Пара</span><b>${esc(d.market||obj.market||'—')}</b></div><div class="line"><span>Действие</span><b>${esc(actionRu(d.action||obj.action))}</b></div><div class="line"><span>Оценка</span><b>${Number(d.score??obj.score??0).toFixed(1)}/100</b></div><div class="line"><span>Уверенность</span><b>${Number(d.confidence??obj.confidence??0).toFixed(1)}%</b></div><div class="line"><span>Цена</span><b>${Number(ind.last_price??obj.last_price??0).toLocaleString('ru-RU')}</b></div><div style="padding-top:10px" class="muted">${esc(d.reason||obj.reason||obj.message||'')}</div>`} function renderDecision(obj){const d=obj.decision||obj,ind=d.indicators||obj.indicators||{};$('aiResult').innerHTML=`<div class="line"><span>Пара</span><b>${esc(d.market||obj.market||'—')}</b></div><div class="line"><span>Действие</span><b>${esc(actionRu(d.action||obj.action))}</b></div><div class="line"><span>Оценка</span><b>${Number(d.score??obj.score??0).toFixed(1)}/100</b></div><div class="line"><span>Уверенность</span><b>${Number(d.confidence??obj.confidence??0).toFixed(1)}%</b></div><div class="line"><span>Цена</span><b>${Number(ind.last_price??obj.last_price??0).toLocaleString('ru-RU')}</b></div><div style="padding-top:10px" class="muted">${esc(d.reason||obj.reason||obj.message||'')}</div>`}
function renderSettings(s){$('modeLabel').textContent=(s.trade_mode||'demo').toUpperCase();$('modeLabel').style.color=s.trade_mode==='live'?'var(--bad)':'var(--good)';$('limitAmount').value=s.max_quote_per_trade??100;$('limitScore').value=s.min_signal_score??65;$('limitPositions').value=s.max_open_positions??3;$('limitBox').innerHTML=`<div class="line"><span>Счет</span><b>${s.trade_mode==='live'?'Live':'Demo'}</b></div><div class="line"><span>Сумма сделки</span><b>${Number(s.max_quote_per_trade||0).toFixed(2)} USDT</b></div><div class="line"><span>Мин. AI</span><b>${Number(s.min_signal_score||0).toFixed(0)}/100</b></div><div class="line"><span>Макс. позиций</span><b>${s.max_open_positions}</b></div>`} function syncLiveToggles(isLive){if($('liveToggle'))$('liveToggle').checked=isLive;if($('liveToggleLimits'))$('liveToggleLimits').checked=isLive}
function renderSettings(s){const isLive=(s.trade_mode||'demo')==='live';$('modeLabel').textContent=isLive?'LIVE':'DEMO';$('modeLabel').style.color=isLive?'var(--bad)':'var(--good)';syncLiveToggles(isLive);$('limitAmount').value=s.max_quote_per_trade??100;$('limitScore').value=s.min_signal_score??65;$('limitPositions').value=s.max_open_positions??3;$('limitBox').innerHTML=`<div class="line"><span>Счет</span><b>${isLive?'Live':'Demo'}</b></div><div class="line"><span>Сумма сделки</span><b>${Number(s.max_quote_per_trade||0).toFixed(2)} USDT</b></div><div class="line"><span>Мин. AI</span><b>${Number(s.min_signal_score||0).toFixed(0)}/100</b></div><div class="line"><span>Макс. позиций</span><b>${s.max_open_positions}</b></div>`}
function renderMonitor(m){if(!m)return;$('monitorLabel').textContent=m.running?'ON':'OFF';$('monitorLabel').style.color=m.running?'var(--good)':'var(--bad)';$('monitorBox').innerHTML=`<div class="line"><span>Статус</span><b>${esc(m.status||'—')}</b></div><div class="line"><span>Последняя пара</span><b>${esc(m.last_market||'—')}</b></div><div class="line"><span>Действие</span><b>${esc(actionRu(m.last_action))}</b></div><div class="line"><span>Оценка</span><b>${Number(m.last_score||0).toFixed(1)}</b></div>${m.last_error?`<div class="dangerText">Ошибка: ${esc(m.last_error)}</div>`:''}`} function renderMonitor(m){if(!m)return;$('monitorLabel').textContent=m.running?'ON':'OFF';$('monitorLabel').style.color=m.running?'var(--good)':'var(--bad)';$('monitorBox').innerHTML=`<div class="line"><span>Статус</span><b>${esc(m.status||'—')}</b></div><div class="line"><span>Последняя пара</span><b>${esc(m.last_market||'—')}</b></div><div class="line"><span>Действие</span><b>${esc(actionRu(m.last_action))}</b></div><div class="line"><span>Оценка</span><b>${Number(m.last_score||0).toFixed(1)}</b></div>${m.last_error?`<div class="dangerText">Ошибка: ${esc(m.last_error)}</div>`:''}`}
function renderKpi(k){$('pnl').textContent=Number(k.realized_pnl||0).toFixed(2)+' USDT';$('openTrades').textContent=k.open_positions??0} function renderKpi(k){$('pnl').textContent=Number(k.realized_pnl||0).toFixed(2)+' USDT';$('openTrades').textContent=k.open_positions??0}
async function bootstrap(){const r=await fetch('/api/v1/app/bootstrap');const j=await r.json();$('marketSelect').innerHTML='';(j.markets||['BTCUSDT']).forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;if(m===j.default_market)o.selected=true;$('marketSelect').appendChild(o)});renderAccount(j.account);renderSettings(j.bot);renderMonitor(j.monitor);if(j.legacy)renderLegacy(j.legacy);await loadSelectedMarket();setInterval(refreshMonitor,5000);setInterval(loadLegacy,8000)} async function bootstrap(){const r=await fetch('/api/v1/app/bootstrap');const j=await r.json();$('marketSelect').innerHTML='';(j.markets||['BTCUSDT']).forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;if(m===j.default_market)o.selected=true;$('marketSelect').appendChild(o)});renderAccount(j.account);renderSettings(j.bot);renderMonitor(j.monitor);if(j.legacy)renderLegacy(j.legacy);await loadSelectedMarket();setInterval(refreshMonitor,5000);setInterval(loadLegacy,8000)}
@@ -101,7 +102,7 @@ function normalizeKline(raw){const rows=raw.data||[];const c=rows.map(r=>({time:
function updateLiveCandle(p,t){if(!Number.isFinite(p))return;const b=Math.floor((t||Date.now())/60000)*60;if(!currentCandle||currentCandle.time!==b)currentCandle={time:b,open:p,high:p,low:p,close:p};else{currentCandle.high=Math.max(currentCandle.high,p);currentCandle.low=Math.min(currentCandle.low,p);currentCandle.close=p}candleSeries.update(currentCandle)} function updateLiveCandle(p,t){if(!Number.isFinite(p))return;const b=Math.floor((t||Date.now())/60000)*60;if(!currentCandle||currentCandle.time!==b)currentCandle={time:b,open:p,high:p,low:p,close:p};else{currentCandle.high=Math.max(currentCandle.high,p);currentCandle.low=Math.min(currentCandle.low,p);currentCandle.close=p}candleSeries.update(currentCandle)}
async function loadSelectedMarket(){const m=getMarket();$('status').textContent='загрузка...';const r=await fetch(`/api/v1/market/kline?market=${m}&period=1min&limit=200`);candleSeries.setData(normalizeKline(await r.json()));chart.timeScale().fitContent();await loadMarkers(m);connectWs(m);await loadAccount();await loadLegacy()} async function loadSelectedMarket(){const m=getMarket();$('status').textContent='загрузка...';const r=await fetch(`/api/v1/market/kline?market=${m}&period=1min&limit=200`);candleSeries.setData(normalizeKline(await r.json()));chart.timeScale().fitContent();await loadMarkers(m);connectWs(m);await loadAccount();await loadLegacy()}
async function loadAccount(){const r=await fetch('/api/v1/demo/account');renderAccount(await r.json())} async function loadMarkers(m){const r=await fetch(`/api/v1/dashboard/markers?market=${m}&limit=100`);const j=await r.json();candleSeries.setMarkers((j.items||[]).map(i=>({time:i.time,position:i.side==='buy'?'belowBar':'aboveBar',shape:i.side==='buy'?'arrowUp':'arrowDown',text:i.text})))} async function loadAccount(){const r=await fetch('/api/v1/demo/account');renderAccount(await r.json())} async function loadMarkers(m){const r=await fetch(`/api/v1/dashboard/markers?market=${m}&limit=100`);const j=await r.json();candleSeries.setMarkers((j.items||[]).map(i=>({time:i.time,position:i.side==='buy'?'belowBar':'aboveBar',shape:i.side==='buy'?'arrowUp':'arrowDown',text:i.text})))}
async function autoPickMarket(){$('status').textContent='AI выбирает пару...';const r=await fetch('/api/v1/bot/best-market');const j=await r.json();if(j.market){$('marketSelect').value=j.market;renderDecision(j);await loadSelectedMarket()}else renderDecision(j)} async function analyze(){const r=await fetch(`/api/v1/bot/analyze?market=${getMarket()}`);renderDecision(await r.json())} async function decide(){const r=await fetch(`/api/v1/bot/decide?market=${getMarket()}`,{method:'POST'});renderDecision(await r.json())} async function autoTrade(){const r=await fetch(`/api/v1/bot/auto-trade?market=${getMarket()}`,{method:'POST'});renderDecision(await r.json());await loadAccount();await loadMarkers(getMarket());await loadLegacy()} async function setMode(mode){const r=await fetch(`/api/v1/bot/settings?trade_mode=${mode}`,{method:'POST'});renderSettings(await r.json())} async function autoPickMarket(){$('status').textContent='AI выбирает пару...';const r=await fetch('/api/v1/bot/best-market');const j=await r.json();if(j.market){$('marketSelect').value=j.market;renderDecision(j);await loadSelectedMarket()}else renderDecision(j)} async function analyze(){const r=await fetch(`/api/v1/bot/analyze?market=${getMarket()}`);renderDecision(await r.json())} async function decide(){const r=await fetch(`/api/v1/bot/decide?market=${getMarket()}`,{method:'POST'});renderDecision(await r.json())} async function autoTrade(){const r=await fetch(`/api/v1/bot/auto-trade?market=${getMarket()}`,{method:'POST'});renderDecision(await r.json());await loadAccount();await loadMarkers(getMarket());await loadLegacy()} async function toggleLiveMode(isLive){const r=await fetch(`/api/v1/bot/settings?trade_mode=${isLive?'live':'demo'}`,{method:'POST'});renderSettings(await r.json())}
async function saveLimits(){const q=`max_quote_per_trade=${encodeURIComponent($('limitAmount').value)}&min_signal_score=${encodeURIComponent($('limitScore').value)}&max_open_positions=${encodeURIComponent($('limitPositions').value)}`;const r=await fetch(`/api/v1/bot/settings?${q}`,{method:'POST'});renderSettings(await r.json())} async function saveLimits(){const q=`max_quote_per_trade=${encodeURIComponent($('limitAmount').value)}&min_signal_score=${encodeURIComponent($('limitScore').value)}&max_open_positions=${encodeURIComponent($('limitPositions').value)}`;const r=await fetch(`/api/v1/bot/settings?${q}`,{method:'POST'});renderSettings(await r.json())}
async function startMonitor(){const r=await fetch('/api/v1/monitor/start',{method:'POST'});const j=await r.json();renderMonitor(j);if(j.bot)renderSettings(j.bot)} async function stopMonitor(){const r=await fetch('/api/v1/monitor/stop',{method:'POST'});const j=await r.json();renderMonitor(j);if(j.bot)renderSettings(j.bot)} async function refreshMonitor(){const r=await fetch('/api/v1/monitor/status');renderMonitor(await r.json())} async function startMonitor(){const r=await fetch('/api/v1/monitor/start',{method:'POST'});const j=await r.json();renderMonitor(j);if(j.bot)renderSettings(j.bot)} async function stopMonitor(){const r=await fetch('/api/v1/monitor/stop',{method:'POST'});const j=await r.json();renderMonitor(j);if(j.bot)renderSettings(j.bot)} async function refreshMonitor(){const r=await fetch('/api/v1/monitor/status');renderMonitor(await r.json())}
async function loadLegacy(){const r=await fetch('/api/v1/legacy/dashboard');renderLegacy(await r.json());loadTrades()} function renderLegacy(j){renderKpi(j.kpi||{});$('strategyTable').innerHTML=(j.strategies||[]).map(s=>`<tr><td>${esc(s.name)}</td><td>${esc(s.timeframe)}</td><td>${esc(s.markets)}</td><td>${s.enabled?'Вкл':'Выкл'}</td><td><button onclick="strategy(${s.id},${!s.enabled})">${s.enabled?'Стоп':'Старт'}</button></td></tr>`).join('');$('coinexBox').innerHTML=`<div class="line"><span>Статус</span><b>${j.coinex?.enabled?'Вкл':'Выкл'}</b></div><div class="line"><span>Режим</span><b>${esc(j.coinex?.mode||'demo')}</b></div><div class="line"><span>Access ID</span><b>${esc(j.coinex?.access_id||'—')}</b></div><div class="line"><span>Secret</span><b>${esc(j.coinex?.secret_masked||'—')}</b></div>`;$('telegramBox').innerHTML=`<div class="line"><span>Статус</span><b>${j.telegram?.enabled?'Вкл':'Выкл'}</b></div><div class="line"><span>Chat ID</span><b>${esc(j.telegram?.chat_id||'—')}</b></div>`} async function loadLegacy(){const r=await fetch('/api/v1/legacy/dashboard');renderLegacy(await r.json());loadTrades()} function renderLegacy(j){renderKpi(j.kpi||{});$('strategyTable').innerHTML=(j.strategies||[]).map(s=>`<tr><td>${esc(s.name)}</td><td>${esc(s.timeframe)}</td><td>${esc(s.markets)}</td><td>${s.enabled?'Вкл':'Выкл'}</td><td><button onclick="strategy(${s.id},${!s.enabled})">${s.enabled?'Стоп':'Старт'}</button></td></tr>`).join('');$('coinexBox').innerHTML=`<div class="line"><span>Статус</span><b>${j.coinex?.enabled?'Вкл':'Выкл'}</b></div><div class="line"><span>Режим</span><b>${esc(j.coinex?.mode||'demo')}</b></div><div class="line"><span>Access ID</span><b>${esc(j.coinex?.access_id||'—')}</b></div><div class="line"><span>Secret</span><b>${esc(j.coinex?.secret_masked||'—')}</b></div>`;$('telegramBox').innerHTML=`<div class="line"><span>Статус</span><b>${j.telegram?.enabled?'Вкл':'Выкл'}</b></div><div class="line"><span>Chat ID</span><b>${esc(j.telegram?.chat_id||'—')}</b></div>`}