Commit ·
e3f3bc4
1
Parent(s): 4f3b44c
Add interactive demo trade chart with draggable lines
Browse files- crypto_ui/terminal.html +122 -3
crypto_ui/terminal.html
CHANGED
|
@@ -6,6 +6,7 @@
|
|
| 6 |
<title>Kronos — Demo Terminal</title>
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
| 9 |
<style>
|
| 10 |
/* ===== design tokens (matches tracker.html) ===== */
|
| 11 |
:root {
|
|
@@ -270,6 +271,7 @@
|
|
| 270 |
<div class="main">
|
| 271 |
<!-- LEFT: Positions -->
|
| 272 |
<div class="col-left">
|
|
|
|
| 273 |
<div class="pos-grid" id="posGrid"></div>
|
| 274 |
<div id="empty">
|
| 275 |
<div class="big">No demo positions</div>
|
|
@@ -281,7 +283,10 @@
|
|
| 281 |
<div class="col-right">
|
| 282 |
<!-- New Trade Form -->
|
| 283 |
<div class="panel-card">
|
| 284 |
-
<div class="panel-title"
|
|
|
|
|
|
|
|
|
|
| 285 |
<form id="tradeForm" autocomplete="off" onsubmit="return executeTrade(event)">
|
| 286 |
<div class="form-group">
|
| 287 |
<label>Symbol</label>
|
|
@@ -390,9 +395,9 @@ function setDir(dir) {
|
|
| 390 |
calcRR();
|
| 391 |
}
|
| 392 |
|
| 393 |
-
/* ===== live R:R calculation ===== */
|
| 394 |
['fEntry','fStop','fTP1'].forEach(id => {
|
| 395 |
-
document.getElementById(id).addEventListener('input', calcRR);
|
| 396 |
});
|
| 397 |
|
| 398 |
function calcRR() {
|
|
@@ -683,6 +688,120 @@ function renderLog() {
|
|
| 683 |
list.scrollTop = list.scrollHeight;
|
| 684 |
}
|
| 685 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 686 |
/* ===== init ===== */
|
| 687 |
(async function init() {
|
| 688 |
await poll();
|
|
|
|
| 6 |
<title>Kronos — Demo Terminal</title>
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<script src="https://unpkg.com/lightweight-charts@4.2.3/dist/lightweight-charts.standalone.production.js"></script>
|
| 10 |
<style>
|
| 11 |
/* ===== design tokens (matches tracker.html) ===== */
|
| 12 |
:root {
|
|
|
|
| 271 |
<div class="main">
|
| 272 |
<!-- LEFT: Positions -->
|
| 273 |
<div class="col-left">
|
| 274 |
+
<div id="chartWrap" style="height:350px;width:100%;margin-bottom:18px;border:1px solid var(--border);border-radius:14px;overflow:hidden;background:var(--panel);display:none;position:relative"></div>
|
| 275 |
<div class="pos-grid" id="posGrid"></div>
|
| 276 |
<div id="empty">
|
| 277 |
<div class="big">No demo positions</div>
|
|
|
|
| 283 |
<div class="col-right">
|
| 284 |
<!-- New Trade Form -->
|
| 285 |
<div class="panel-card">
|
| 286 |
+
<div class="panel-title" style="display:flex;justify-content:space-between;align-items:center">
|
| 287 |
+
Execute Trade
|
| 288 |
+
<button type="button" class="btn-dim" style="margin:0;border-color:var(--accent);color:var(--text);background:rgba(139,92,246,.1)" onclick="loadDemoTrade()">✨ Load Demo Trade</button>
|
| 289 |
+
</div>
|
| 290 |
<form id="tradeForm" autocomplete="off" onsubmit="return executeTrade(event)">
|
| 291 |
<div class="form-group">
|
| 292 |
<label>Symbol</label>
|
|
|
|
| 395 |
calcRR();
|
| 396 |
}
|
| 397 |
|
| 398 |
+
/* ===== live R:R calculation & chart sync ===== */
|
| 399 |
['fEntry','fStop','fTP1'].forEach(id => {
|
| 400 |
+
document.getElementById(id).addEventListener('input', () => { calcRR(); syncChartLines(); });
|
| 401 |
});
|
| 402 |
|
| 403 |
function calcRR() {
|
|
|
|
| 688 |
list.scrollTop = list.scrollHeight;
|
| 689 |
}
|
| 690 |
|
| 691 |
+
/* ===== Interactive Chart & Demo Setup ===== */
|
| 692 |
+
let chart, candleSeries;
|
| 693 |
+
let entryLine, stopLine, tpLine;
|
| 694 |
+
let draggingLine = null; // 'entry', 'stop', or 'tp'
|
| 695 |
+
let lastPrice = 0;
|
| 696 |
+
|
| 697 |
+
function initChart() {
|
| 698 |
+
if (chart) return;
|
| 699 |
+
$('#chartWrap').style.display = 'block';
|
| 700 |
+
chart = LightweightCharts.createChart($('#chartWrap'), {
|
| 701 |
+
layout: { background: { color: '#10121a' }, textColor: '#767d96' },
|
| 702 |
+
grid: { vertLines: { color: 'rgba(27,30,42,0.5)' }, horzLines: { color: 'rgba(27,30,42,0.5)' } },
|
| 703 |
+
crosshair: { mode: 0 },
|
| 704 |
+
rightPriceScale: { borderColor: '#1b1e2a' },
|
| 705 |
+
timeScale: { borderColor: '#1b1e2a', timeVisible: true }
|
| 706 |
+
});
|
| 707 |
+
|
| 708 |
+
candleSeries = chart.addCandlestickSeries({
|
| 709 |
+
upColor: '#16c784', downColor: '#ea3943', borderVisible: false,
|
| 710 |
+
wickUpColor: '#16c784', wickDownColor: '#ea3943'
|
| 711 |
+
});
|
| 712 |
+
|
| 713 |
+
// Setup drag interaction using subscribeCrosshairMove and mousedown/up
|
| 714 |
+
const container = $('#chartWrap');
|
| 715 |
+
let isMousedown = false;
|
| 716 |
+
|
| 717 |
+
container.addEventListener('mousedown', () => { isMousedown = true; });
|
| 718 |
+
container.addEventListener('mouseup', () => { isMousedown = false; draggingLine = null; calcRR(); });
|
| 719 |
+
container.addEventListener('mouseleave', () => { isMousedown = false; draggingLine = null; calcRR(); });
|
| 720 |
+
|
| 721 |
+
chart.subscribeCrosshairMove(param => {
|
| 722 |
+
if (!param.point || !param.price) return;
|
| 723 |
+
const p = param.price;
|
| 724 |
+
|
| 725 |
+
// Determine which line to drag on mousedown
|
| 726 |
+
if (isMousedown && !draggingLine) {
|
| 727 |
+
const ePrice = parseFloat($('#fEntry').value) || 0;
|
| 728 |
+
const sPrice = parseFloat($('#fStop').value) || 0;
|
| 729 |
+
const tPrice = parseFloat($('#fTP1').value) || 0;
|
| 730 |
+
|
| 731 |
+
// Calculate distances in pixels to make it easier to grab
|
| 732 |
+
const yClick = param.point.y;
|
| 733 |
+
const yEntry = candleSeries.priceToCoordinate(ePrice);
|
| 734 |
+
const yStop = candleSeries.priceToCoordinate(sPrice);
|
| 735 |
+
const yTP = candleSeries.priceToCoordinate(tPrice);
|
| 736 |
+
|
| 737 |
+
let closest = null;
|
| 738 |
+
let minD = 20; // 20px grab radius
|
| 739 |
+
|
| 740 |
+
if (yEntry && Math.abs(yClick - yEntry) < minD) { closest = 'entry'; minD = Math.abs(yClick - yEntry); }
|
| 741 |
+
if (yStop && Math.abs(yClick - yStop) < minD) { closest = 'stop'; minD = Math.abs(yClick - yStop); }
|
| 742 |
+
if (yTP && Math.abs(yClick - yTP) < minD) { closest = 'tp'; minD = Math.abs(yClick - yTP); }
|
| 743 |
+
|
| 744 |
+
draggingLine = closest;
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
if (draggingLine) {
|
| 748 |
+
if (draggingLine === 'entry') { $('#fEntry').value = Number(p).toFixed(2); entryLine.applyOptions({ price: p }); }
|
| 749 |
+
if (draggingLine === 'stop') { $('#fStop').value = Number(p).toFixed(2); stopLine.applyOptions({ price: p }); }
|
| 750 |
+
if (draggingLine === 'tp') { $('#fTP1').value = Number(p).toFixed(2); tpLine.applyOptions({ price: p }); }
|
| 751 |
+
// Don't calcRR on every tick to save perf, do it on mouseup
|
| 752 |
+
}
|
| 753 |
+
});
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
function syncChartLines() {
|
| 757 |
+
if (!chart || !candleSeries) return;
|
| 758 |
+
const ePrice = parseFloat($('#fEntry').value) || 0;
|
| 759 |
+
const sPrice = parseFloat($('#fStop').value) || 0;
|
| 760 |
+
const tPrice = parseFloat($('#fTP1').value) || 0;
|
| 761 |
+
|
| 762 |
+
if (entryLine) candleSeries.removePriceLine(entryLine);
|
| 763 |
+
if (stopLine) candleSeries.removePriceLine(stopLine);
|
| 764 |
+
if (tpLine) candleSeries.removePriceLine(tpLine);
|
| 765 |
+
|
| 766 |
+
if (ePrice) entryLine = candleSeries.createPriceLine({ price: ePrice, color: '#e8eaf2', lineWidth: 2, lineStyle: 0, title: 'Entry (drag)' });
|
| 767 |
+
if (sPrice) stopLine = candleSeries.createPriceLine({ price: sPrice, color: '#ea3943', lineWidth: 2, lineStyle: 0, title: 'Stop' });
|
| 768 |
+
if (tPrice) tpLine = candleSeries.createPriceLine({ price: tPrice, color: '#16c784', lineWidth: 2, lineStyle: 0, title: 'TP1' });
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
async function loadDemoTrade() {
|
| 772 |
+
$('#fSym').value = 'BTCUSDT';
|
| 773 |
+
$('#fProvider').value = 'binance';
|
| 774 |
+
|
| 775 |
+
// Fetch current price to populate realistic demo targets
|
| 776 |
+
initChart();
|
| 777 |
+
try {
|
| 778 |
+
const r = await fetch('/api/klines?provider=binance&symbol=BTCUSDT&interval=15m');
|
| 779 |
+
const data = await r.json();
|
| 780 |
+
if (data.klines && data.klines.length) {
|
| 781 |
+
candleSeries.setData(data.klines.map(k => ({
|
| 782 |
+
time: k.time / 1000, open: k.open, high: k.high, low: k.low, close: k.close
|
| 783 |
+
})));
|
| 784 |
+
lastPrice = data.klines[data.klines.length - 1].close;
|
| 785 |
+
|
| 786 |
+
// Populate form (Long by default)
|
| 787 |
+
setDir('long');
|
| 788 |
+
$('#fEntry').value = lastPrice.toFixed(2);
|
| 789 |
+
$('#fStop').value = (lastPrice * 0.985).toFixed(2); // -1.5%
|
| 790 |
+
$('#fTP1').value = (lastPrice * 1.035).toFixed(2); // +3.5% (approx 2.3 R:R)
|
| 791 |
+
$('#fLev').value = '5';
|
| 792 |
+
$('#fQty').value = '0.5';
|
| 793 |
+
|
| 794 |
+
calcRR();
|
| 795 |
+
syncChartLines();
|
| 796 |
+
|
| 797 |
+
// Auto-scale chart to fit the trade setup
|
| 798 |
+
setTimeout(() => { chart.timeScale().fitContent(); }, 100);
|
| 799 |
+
}
|
| 800 |
+
} catch (err) {
|
| 801 |
+
console.error('Failed to load chart data:', err);
|
| 802 |
+
}
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
/* ===== init ===== */
|
| 806 |
(async function init() {
|
| 807 |
await poll();
|