trtr

Trading simulator and techanalysis gym
git clone https://git.ea.contact/trtr
Log | Files | Refs

commit d045b06f07d355f810b35a1ba018e53d3a442ad5
parent d5b1593b4d2cee8e967368fffd26a4669ce651f0
Author: ea <ea@ea.contact>
Date:   Sun, 24 May 2026 15:30:21 +0000

Add techalyzis gym mod

Diffstat:
Mfrontend/index.html | 517++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 516 insertions(+), 1 deletion(-)

diff --git a/frontend/index.html b/frontend/index.html @@ -362,6 +362,128 @@ transition: background 0.1s; } .sell-from-buy-btn:hover { background: #4a1818; } + + /* ── Gym Mode ────────────────────────────────────────────────────────── */ + + #gym-mode-btn { + background: #1a1a3a; + color: #7070a0; + border: 1px solid #3a3a6a; + border-radius: 6px; + padding: 6px 14px; + font-size: 0.8rem; + cursor: pointer; + transition: background 0.15s, color 0.15s; + } + #gym-mode-btn:hover { background: #28285a; color: #c0c0e0; } + #gym-mode-btn.gym-active { + background: #28105a; + color: #b090ff; + border-color: #6040b0; + } + + #gym-canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 2; + } + + #gym-sidebar { + display: none; + width: 220px; + flex-shrink: 0; + background: #16162a; + border-left: 1px solid #2a2a4a; + flex-direction: column; + padding: 16px; + gap: 18px; + overflow-y: auto; + } + + .gym-section { display: flex; flex-direction: column; gap: 8px; } + + .gym-section h2 { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 1px; + color: #505070; + margin-bottom: 2px; + } + + .gym-tool-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 5px; + } + + .gym-tool-btn { + background: #1e1e36; + color: #606080; + border: 1px solid #3a3a5a; + border-radius: 5px; + padding: 7px 4px; + font-size: 0.75rem; + cursor: pointer; + transition: background 0.1s, color 0.1s; + text-align: center; + } + .gym-tool-btn:hover { background: #2a2a4a; color: #a0a0c0; } + .gym-tool-btn.active { + background: #28105a; + color: #c0a0ff; + border-color: #5030a0; + } + + .gym-hint { + font-size: 0.7rem; + color: #404060; + line-height: 1.45; + min-height: 2.2em; + } + + #prediction-info { + background: #1a1a30; + border: 1px solid #3a3a5a; + border-radius: 5px; + padding: 8px; + font-size: 0.72rem; + color: #505070; + line-height: 1.7; + min-height: 64px; + font-variant-numeric: tabular-nums; + } + #prediction-info.has-pred { border-color: #5030a0; color: #c0a0ff; } + + .gym-btn { + width: 100%; + background: #1e1e36; + border: 1px solid #3a3a5a; + border-radius: 5px; + color: #505070; + font-size: 0.75rem; + padding: 6px 0; + cursor: pointer; + transition: background 0.1s, color 0.1s; + } + .gym-btn:hover { background: #2a2a4a; color: #a0a0c0; } + .gym-btn.danger:hover { background: #3a1a1a; color: #ef5350; border-color: #5a2020; } + + #text-input-overlay { + position: fixed; + background: #1e1e36; + border: 1px solid #5030a0; + border-radius: 4px; + padding: 3px 7px; + color: #e0d8ff; + font-size: 0.82rem; + z-index: 200; + outline: none; + min-width: 110px; + display: none; + } </style> </head> <body> @@ -402,6 +524,7 @@ <option value="11">Nov</option> <option value="12">Dec</option> </select> + <button id="gym-mode-btn">Techalyzis Gym</button> <button id="new-session-btn">New Session</button> </div> </header> @@ -410,10 +533,11 @@ <div id="chart-container"> <div id="chart"></div> <svg id="be-overlay" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:1"></svg> + <canvas id="gym-canvas"></canvas> <div id="loading-overlay">Loading data&hellip;</div> </div> - <div class="sidebar"> + <div class="sidebar" id="sim-sidebar"> <div class="portfolio"> <h2>Portfolio</h2> <div class="stat"> @@ -486,8 +610,37 @@ </div> </div> </div> + <div id="gym-sidebar"> + <div class="gym-section"> + <h2>Drawing Tools</h2> + <div class="gym-tool-grid"> + <button class="gym-tool-btn" id="tool-line">Line</button> + <button class="gym-tool-btn" id="tool-pencil">Pencil</button> + <button class="gym-tool-btn" id="tool-text">Text</button> + <button class="gym-tool-btn" id="tool-predict">Predict</button> + </div> + <p class="gym-hint" id="tool-hint">Select a tool or pan/zoom freely.</p> + </div> + + <div class="gym-section"> + <h2>Active Prediction</h2> + <div id="prediction-info">No prediction set</div> + <button class="gym-btn danger" id="clear-prediction-btn">Cancel prediction</button> + </div> + + <div class="gym-section"> + <h2>Score</h2> + <div id="gym-score" class="pnl-zero" style="font-size:1.1rem;font-weight:700;font-variant-numeric:tabular-nums">$0.00</div> + </div> + + <div class="gym-section" style="margin-top:auto"> + <button class="gym-btn danger" id="clear-drawings-btn">Clear all drawings</button> + </div> + </div> </div> +<input id="text-input-overlay" type="text" placeholder="Type and press Enter"> + <footer> <button id="next-btn" disabled>&#9654;&#9654; Next Candle</button> </footer> @@ -499,6 +652,7 @@ let currentPrice = 0; let usdBalance = 10000; let btcBalance = 0; + let gymLastLogical = 0; // ── Trade lines ─────────────────────────────────────────────────────────── @@ -786,6 +940,9 @@ setEnabled(false); document.getElementById('loading-overlay').style.display = 'flex'; if (candleSeries) { clearTradeLines(); clearCurrentBreakEven(); } + gymPrediction = null; + gymScore = 0; + if (gymMode) { updatePredictionInfo(); updateGymScore(); } const year = document.getElementById('year-select').value; const month = document.getElementById('month-select').value; @@ -812,6 +969,7 @@ })); candleSeries.setData(lwcCandles); chart.timeScale().fitContent(); + gymLastLogical = lwcCandles.length - 1; currentPrice = data.candles[data.candles.length - 1].close; updatePortfolio(data.usd, data.btc, data.pnl, currentPrice); @@ -927,18 +1085,375 @@ const c = data.candle; candleSeries.update({ time: c.ts, open: c.open, high: c.high, low: c.low, close: c.close }); chart.timeScale().scrollToRealTime(); + gymLastLogical++; currentPrice = c.close; updatePortfolio(data.usd, data.btc, data.pnl, c.close); syncCurrentBreakEven(); + if (gymMode) { + if (gymPrediction && gymLastLogical >= gymPrediction.logTarget) { + resolveGymPrediction(c.close); + } else { + updatePredictionInfo(); + } + } }); // ── New session button ──────────────────────────────────────────────────── document.getElementById('new-session-btn').addEventListener('click', startSession); + // ── Gym Mode ────────────────────────────────────────────────────────────── + + let gymMode = false; + let gymActiveTool = null; // null = no tool, chart pans/zooms freely + const gymDrawings = []; + let gymPrediction = null; // { logTarget, entryPrice, direction: 'long'|'short' } + let gymScore = 0; + let gymIsDrawing = false; + let gymCurrentPoints = []; + let gymTextPending = null; + let gymHoverPos = null; // {logical, price} for predict hover preview + + const gymCanvas = document.getElementById('gym-canvas'); + const gymCtx = gymCanvas.getContext('2d'); + const textInputEl = document.getElementById('text-input-overlay'); + + function resizeGymCanvas() { + const r = document.getElementById('chart-container').getBoundingClientRect(); + gymCanvas.width = r.width; + gymCanvas.height = r.height; + } + + window.addEventListener('resize', resizeGymCanvas); + + function gymPos(e) { + const r = gymCanvas.getBoundingClientRect(); + return { x: e.clientX - r.left, y: e.clientY - r.top }; + } + + function pxToChart(x, y) { + return { + logical: chart.timeScale().coordinateToLogical(x), + price: candleSeries.coordinateToPrice(y), + }; + } + + function chartToPx(logical, price) { + return { + x: chart.timeScale().logicalToCoordinate(logical), + y: candleSeries.priceToCoordinate(price), + }; + } + + gymCanvas.addEventListener('mousedown', e => { + if (!gymMode) return; + const { x, y } = gymPos(e); + const { logical, price } = pxToChart(x, y); + if (logical == null || price == null) return; + gymIsDrawing = true; + gymCurrentPoints = [{ logical, price }]; + }); + + gymCanvas.addEventListener('mousemove', e => { + if (!gymMode) return; + const { x, y } = gymPos(e); + const { logical, price } = pxToChart(x, y); + gymHoverPos = (logical != null && price != null) ? { logical, price } : null; + if (!gymIsDrawing) return; + if (logical == null || price == null) return; + if (gymActiveTool === 'pencil') { + gymCurrentPoints.push({ logical, price }); + } else { + gymCurrentPoints[1] = { logical, price }; + } + }); + + gymCanvas.addEventListener('mouseup', e => { + if (!gymMode || !gymIsDrawing) return; + gymIsDrawing = false; + const { x, y } = gymPos(e); + const { logical, price } = pxToChart(x, y); + + if (gymActiveTool === 'line' && gymCurrentPoints.length >= 2) { + gymDrawings.push({ type: 'line', points: [...gymCurrentPoints] }); + } else if (gymActiveTool === 'pencil' && gymCurrentPoints.length >= 2) { + gymDrawings.push({ type: 'pencil', points: [...gymCurrentPoints] }); + } else if (gymActiveTool === 'text') { + if (logical != null && price != null) { + gymTextPending = { logical, price }; + const cr = gymCanvas.getBoundingClientRect(); + textInputEl.style.left = (cr.left + x + 4) + 'px'; + textInputEl.style.top = (cr.top + y - 14) + 'px'; + textInputEl.style.display = 'block'; + textInputEl.value = ''; + textInputEl.focus(); + } + } else if (gymActiveTool === 'predict') { + const pt = gymCurrentPoints[0]; + if (pt && Math.round(pt.logical) > gymLastLogical) { + gymPrediction = { + logTarget: Math.round(pt.logical), + entryPrice: currentPrice, + direction: pt.price > currentPrice ? 'long' : 'short', + }; + updatePredictionInfo(); + } + } + gymCurrentPoints = []; + }); + + gymCanvas.addEventListener('mouseleave', () => { + gymHoverPos = null; + if (gymIsDrawing && gymActiveTool === 'pencil' && gymCurrentPoints.length >= 2) { + gymDrawings.push({ type: 'pencil', points: [...gymCurrentPoints] }); + } + gymIsDrawing = false; + gymCurrentPoints = []; + }); + + textInputEl.addEventListener('keydown', e => { + if (e.key === 'Enter') { + const text = textInputEl.value.trim(); + if (text && gymTextPending) { + gymDrawings.push({ type: 'text', points: [{ ...gymTextPending }], text }); + } + textInputEl.style.display = 'none'; + gymTextPending = null; + } else if (e.key === 'Escape') { + textInputEl.style.display = 'none'; + gymTextPending = null; + } + }); + + function updatePredictionInfo() { + const el = document.getElementById('prediction-info'); + if (!gymPrediction) { + el.textContent = 'No prediction set'; + el.classList.remove('has-pred'); + return; + } + const { logTarget, entryPrice, direction } = gymPrediction; + const barsLeft = Math.max(0, logTarget - gymLastLogical); + const dir = direction === 'long' ? '▲ LONG' : '▼ SHORT'; + const col = direction === 'long' ? '#26a69a' : '#ef5350'; + el.classList.add('has-pred'); + el.innerHTML = + `<span style="color:${col};font-weight:700">${dir}</span><br>` + + `Entry:&nbsp; $${fmt(entryPrice)}<br>` + + `Closes: ${barsLeft === 0 ? 'next bar' : '+' + barsLeft + ' bars'}`; + } + + function updateGymScore() { + const el = document.getElementById('gym-score'); + el.textContent = (gymScore >= 0 ? '+' : '') + '$' + fmt(gymScore); + el.className = gymScore > 0 ? 'pnl-positive' : gymScore < 0 ? 'pnl-negative' : 'pnl-zero'; + el.style.cssText += ';font-size:1.1rem;font-weight:700;font-variant-numeric:tabular-nums'; + } + + function resolveGymPrediction(closePrice) { + if (!gymPrediction) return; + const { entryPrice, direction } = gymPrediction; + const pct = direction === 'long' + ? (closePrice - entryPrice) / entryPrice + : (entryPrice - closePrice) / entryPrice; + gymScore += pct * 1000; + gymPrediction = null; + updatePredictionInfo(); + updateGymScore(); + } + + function gymDrawFrame() { + requestAnimationFrame(gymDrawFrame); + const ctx = gymCtx; + ctx.clearRect(0, 0, gymCanvas.width, gymCanvas.height); + if (!gymMode) return; + + // Prediction marker + if (gymPrediction) { + const { logTarget, entryPrice, direction } = gymPrediction; + const tx = chart.timeScale().logicalToCoordinate(logTarget); + const ey = candleSeries.priceToCoordinate(entryPrice); + const col = direction === 'long' ? '#26a69a' : '#ef5350'; + if (tx != null && ey != null) { + // Vertical line at target bar + ctx.strokeStyle = col; + ctx.lineWidth = 1.5; + ctx.setLineDash([5, 4]); + ctx.globalAlpha = 0.55; + ctx.beginPath(); + ctx.moveTo(tx, 0); + ctx.lineTo(tx, gymCanvas.height); + ctx.stroke(); + ctx.setLineDash([]); + + // Horizontal entry-price line + ctx.beginPath(); + ctx.moveTo(0, ey); + ctx.lineTo(gymCanvas.width, ey); + ctx.stroke(); + ctx.globalAlpha = 1; + + // Triangle arrow at intersection + const arrowDir = direction === 'long' ? -1 : 1; // -1 = up, 1 = down + const aw = 9, ah = 14; + ctx.fillStyle = col; + ctx.beginPath(); + ctx.moveTo(tx, ey + arrowDir * ah); + ctx.lineTo(tx - aw, ey - arrowDir * 4); + ctx.lineTo(tx + aw, ey - arrowDir * 4); + ctx.closePath(); + ctx.fill(); + + // Label + ctx.fillStyle = col; + ctx.font = 'bold 11px "Segoe UI", system-ui, sans-serif'; + const label = direction === 'long' ? 'LONG' : 'SHORT'; + ctx.fillText(label, tx + 12, ey - 4); + ctx.fillStyle = 'rgba(200,200,200,0.7)'; + ctx.font = '10px "Segoe UI", system-ui, sans-serif'; + ctx.fillText('$' + fmt(entryPrice), tx + 12, ey + 10); + } + } + + // In-progress preview (line/pencil drag) + if (gymIsDrawing && gymActiveTool !== 'predict' && gymCurrentPoints.length >= 1) { + renderGymShape(ctx, gymActiveTool, gymCurrentPoints, null, 0.55); + } + + // Predict hover preview — vertical guide while hovering + if (gymActiveTool === 'predict' && gymHoverPos) { + const { logical, price } = gymHoverPos; + if (logical != null && Math.round(logical) > gymLastLogical) { + const hx = chart.timeScale().logicalToCoordinate(logical); + const ey = candleSeries.priceToCoordinate(currentPrice); + const isLong = price > currentPrice; + const hcol = isLong ? '#26a69a' : '#ef5350'; + if (hx != null) { + ctx.strokeStyle = hcol; + ctx.lineWidth = 1; + ctx.setLineDash([4, 4]); + ctx.globalAlpha = 0.35; + ctx.beginPath(); + ctx.moveTo(hx, 0); + ctx.lineTo(hx, gymCanvas.height); + ctx.stroke(); + if (ey != null) { + ctx.beginPath(); + ctx.moveTo(0, ey); + ctx.lineTo(gymCanvas.width, ey); + ctx.stroke(); + } + ctx.setLineDash([]); + ctx.globalAlpha = 1; + } + } + } + + // Saved drawings + for (const d of gymDrawings) { + renderGymShape(ctx, d.type, d.points, d.text, 1); + } + } + + function renderGymShape(ctx, type, points, text, alpha) { + const px = points.map(p => chartToPx(p.logical, p.price)); + if (px.length === 0 || px[0].x == null) return; + ctx.globalAlpha = alpha; + + if (type === 'line' || type === 'pencil') { + ctx.strokeStyle = type === 'line' ? '#f0c040' : '#88aaff'; + ctx.lineWidth = 1.8; + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(px[0].x, px[0].y); + for (let i = 1; i < px.length; i++) { + if (px[i].x != null && px[i].y != null) ctx.lineTo(px[i].x, px[i].y); + } + ctx.stroke(); + } else if (type === 'text' && text) { + ctx.fillStyle = '#e0d0ff'; + ctx.font = '13px "Segoe UI", system-ui, sans-serif'; + ctx.fillText(text, px[0].x, px[0].y); + } + + ctx.globalAlpha = 1; + } + + requestAnimationFrame(gymDrawFrame); + + // ── Mode switching ──────────────────────────────────────────────────────── + + const GYM_RIGHT_OFFSET = 55; + + function setGymTool(tool) { + gymActiveTool = tool; + document.querySelectorAll('.gym-tool-btn').forEach(b => b.classList.remove('active')); + if (tool) { + document.getElementById('tool-' + tool).classList.add('active'); + gymCanvas.style.pointerEvents = 'auto'; + gymCanvas.style.cursor = tool === 'text' ? 'text' : 'crosshair'; + } else { + gymCanvas.style.pointerEvents = 'none'; + gymCanvas.style.cursor = 'default'; + } + const hints = { + line: 'Click and drag to draw a straight line.', + pencil: 'Click and drag to draw freehand.', + text: 'Click to place a text label.', + predict: 'Click a future point — above price = Long, below = Short.', + }; + document.getElementById('tool-hint').textContent = + tool ? hints[tool] : 'Select a tool or pan/zoom freely.'; + } + + function enterGymMode() { + gymMode = true; + document.getElementById('gym-mode-btn').textContent = '← Simulator'; + document.getElementById('gym-mode-btn').classList.add('gym-active'); + document.getElementById('sim-sidebar').style.display = 'none'; + document.getElementById('gym-sidebar').style.display = 'flex'; + chart.timeScale().applyOptions({ rightOffset: GYM_RIGHT_OFFSET }); + resizeGymCanvas(); + setGymTool(null); // start with no tool so chart pans freely + updatePredictionInfo(); + updateGymScore(); + } + + function exitGymMode() { + gymMode = false; + document.getElementById('gym-mode-btn').textContent = 'Techalyzis Gym'; + document.getElementById('gym-mode-btn').classList.remove('gym-active'); + document.getElementById('sim-sidebar').style.display = 'flex'; + document.getElementById('gym-sidebar').style.display = 'none'; + gymCanvas.style.pointerEvents = 'none'; + chart.timeScale().applyOptions({ rightOffset: 0 }); + } + + document.getElementById('gym-mode-btn').addEventListener('click', () => { + if (gymMode) exitGymMode(); else enterGymMode(); + }); + + // Tool buttons — click active tool to deselect + ['line', 'pencil', 'text', 'predict'].forEach(tool => { + document.getElementById('tool-' + tool).addEventListener('click', () => { + setGymTool(gymActiveTool === tool ? null : tool); + }); + }); + + document.getElementById('clear-prediction-btn').addEventListener('click', () => { + gymPrediction = null; + updatePredictionInfo(); + }); + + document.getElementById('clear-drawings-btn').addEventListener('click', () => { + gymDrawings.length = 0; + }); + // ── Boot ────────────────────────────────────────────────────────────────── initChart(); + resizeGymCanvas(); startSession(); </script> </body>