trtr

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

commit 20de5c23dd5b9ab0a2bfc8f7e783f6e2879c5112
parent 91c8322b1f34ec8b1d1801a5a6a958ffbd600cf0
Author: ea <ea@ea.contact>
Date:   Sun, 24 May 2026 21:59:20 +0000

add keybindings; double-click for prediction; add play

Diffstat:
Mfrontend/index.html | 174++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 172 insertions(+), 2 deletions(-)

diff --git a/frontend/index.html b/frontend/index.html @@ -222,6 +222,8 @@ border-top: 1px solid #2a2a4a; display: flex; justify-content: center; + align-items: center; + gap: 8px; flex-shrink: 0; } @@ -239,6 +241,52 @@ #next-btn:hover:not(:disabled) { background: #3a3a8a; color: #e0e0ff; } #next-btn:disabled { opacity: 0.4; cursor: not-allowed; } + #auto-play-btn { + background: #2a2a6a; + color: #a0a0e0; + border: 1px solid #4a4a8a; + border-radius: 6px; + width: 36px; + height: 36px; + font-size: 1rem; + cursor: pointer; + transition: background 0.15s, color 0.15s; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + #auto-play-btn:hover:not(:disabled) { background: #3a3a8a; color: #e0e0ff; } + #auto-play-btn:disabled { opacity: 0.4; cursor: not-allowed; } + #auto-play-btn.playing { background: #3a2a6a; color: #d0a0ff; border-color: #7a4aaa; } + + #ff-to-position-btn { + background: #0f2e26; + color: #26a69a; + border: 1px solid #1a4a3a; + border-radius: 6px; + padding: 9px 20px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.15s; + } + #ff-to-position-btn:hover:not(:disabled) { background: #143d32; color: #4fd4c0; border-color: #267a6a; } + #ff-to-position-btn:disabled { opacity: 0.4; cursor: not-allowed; } + + .footer-btn-group { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + } + + .key-hint { + font-size: 0.6rem; + color: #383858; + letter-spacing: 0.5px; + } + .trades-section { display: flex; flex-direction: column; @@ -636,7 +684,6 @@ </span> </div> <div id="prediction-info">No prediction set</div> - <button class="gym-btn go" id="ff-to-position-btn" disabled>&#9654;&#9654; Play to close</button> <button class="gym-btn danger" id="clear-prediction-btn">Cancel prediction</button> </div> @@ -654,7 +701,18 @@ <input id="text-input-overlay" type="text" placeholder="Type and press Enter"> <footer> - <button id="next-btn" disabled>&#9654;&#9654; Next Candle</button> + <div class="footer-btn-group"> + <button id="next-btn" disabled>&#9654;&#9654; Next Candle</button> + <span class="key-hint">Space</span> + </div> + <div class="footer-btn-group"> + <button id="auto-play-btn" disabled title="Auto-play (1 candle/sec)">&#9654;</button> + <span class="key-hint">&nbsp;</span> + </div> + <div class="footer-btn-group" id="ff-btn-group" style="display:none"> + <button id="ff-to-position-btn" disabled>&#9654;&#9654; Play to close</button> + <span class="key-hint">C</span> + </div> </footer> <script> @@ -949,6 +1007,7 @@ }); async function startSession() { + stopAutoPlay(); setEnabled(false); document.getElementById('loading-overlay').style.display = 'flex'; if (candleSeries) { clearTradeLines(); clearCurrentBreakEven(); } @@ -1021,6 +1080,7 @@ document.getElementById('buy-btn').disabled = !enabled; document.getElementById('sell-btn').disabled = !enabled; document.getElementById('next-btn').disabled = !enabled; + document.getElementById('auto-play-btn').disabled = !enabled; } // ── Sliders + mode toggle ───────────────────────────────────────────────── @@ -1111,6 +1171,73 @@ } }); + // ── Auto-play ───────────────────────────────────────────────────────────── + + let autoPlayActive = false; + let autoPlayTimer = null; + + function stopAutoPlay() { + autoPlayActive = false; + clearTimeout(autoPlayTimer); + autoPlayTimer = null; + const btn = document.getElementById('auto-play-btn'); + btn.textContent = '►'; + btn.title = 'Auto-play (1 candle/sec)'; + btn.classList.remove('playing'); + } + + async function autoPlayStep() { + if (!autoPlayActive) return; + + const res = await fetch('/api/next', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId }), + }); + + if (res.status === 409) { + document.getElementById('next-btn').textContent = 'End of data'; + document.getElementById('next-btn').disabled = true; + stopAutoPlay(); + return; + } + + const data = await res.json(); + 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(); + } + } + + if (autoPlayActive) { + autoPlayTimer = setTimeout(autoPlayStep, 1000); + } + } + + function toggleAutoPlay() { + if (autoPlayActive) { + stopAutoPlay(); + } else { + autoPlayActive = true; + const btn = document.getElementById('auto-play-btn'); + btn.textContent = '■'; + btn.title = 'Stop auto-play'; + btn.classList.add('playing'); + autoPlayStep(); + } + } + + document.getElementById('auto-play-btn').addEventListener('click', toggleAutoPlay); + // ── New session button ──────────────────────────────────────────────────── document.getElementById('new-session-btn').addEventListener('click', startSession); @@ -1481,6 +1608,7 @@ document.getElementById('gym-mode-btn').classList.add('gym-active'); document.getElementById('sim-sidebar').style.display = 'none'; document.getElementById('gym-sidebar').style.display = 'flex'; + document.getElementById('ff-btn-group').style.display = ''; chart.timeScale().applyOptions({ rightOffset: GYM_RIGHT_OFFSET }); resizeGymCanvas(); setGymTool(null); // start with no tool so chart pans freely @@ -1494,6 +1622,7 @@ document.getElementById('gym-mode-btn').classList.remove('gym-active'); document.getElementById('sim-sidebar').style.display = 'flex'; document.getElementById('gym-sidebar').style.display = 'none'; + document.getElementById('ff-btn-group').style.display = 'none'; gymCanvas.style.pointerEvents = 'none'; chart.timeScale().applyOptions({ rightOffset: 0 }); } @@ -1516,9 +1645,11 @@ async function gymFastForwardToClose() { if (!gymPrediction || gymFastForwarding) return; + stopAutoPlay(); gymFastForwarding = true; document.getElementById('ff-to-position-btn').disabled = true; document.getElementById('next-btn').disabled = true; + document.getElementById('auto-play-btn').disabled = true; while (gymPrediction && gymLastLogical < gymPrediction.logTarget) { const res = await fetch('/api/next', { @@ -1554,6 +1685,7 @@ gymFastForwarding = false; document.getElementById('next-btn').disabled = false; + document.getElementById('auto-play-btn').disabled = false; updatePredictionInfo(); } @@ -1563,6 +1695,44 @@ gymDrawings.length = 0; }); + // ── Keyboard shortcuts ──────────────────────────────────────────────────── + + document.addEventListener('keydown', e => { + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; + if (e.key === 'Escape') { + if (gymMode && gymActiveTool !== null) setGymTool(null); + } else if (e.key === ' ') { + e.preventDefault(); + const btn = document.getElementById('next-btn'); + if (!btn.disabled) btn.click(); + } else if (e.key === 'c' || e.key === 'C') { + const btn = document.getElementById('ff-to-position-btn'); + if (!btn.disabled) btn.click(); + } + }); + + // ── Double-click prediction (no tool selected) ──────────────────────────── + + document.addEventListener('dblclick', e => { + if (!gymMode || gymActiveTool !== null) return; + if (!document.getElementById('chart-container').contains(e.target)) return; + e.stopPropagation(); + e.preventDefault(); + const r = gymCanvas.getBoundingClientRect(); + const x = e.clientX - r.left; + const y = e.clientY - r.top; + const { logical, price } = pxToChart(x, y); + if (logical == null || price == null) return; + if (Math.round(logical) > gymLastLogical) { + gymPrediction = { + logTarget: Math.round(logical), + entryPrice: currentPrice, + direction: price > currentPrice ? 'long' : 'short', + }; + updatePredictionInfo(); + } + }, true); + // ── Boot ────────────────────────────────────────────────────────────────── initChart();