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:
| M | frontend/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>▶▶ 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>▶▶ Next Candle</button>
+ <div class="footer-btn-group">
+ <button id="next-btn" disabled>▶▶ 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)">▶</button>
+ <span class="key-hint"> </span>
+ </div>
+ <div class="footer-btn-group" id="ff-btn-group" style="display:none">
+ <button id="ff-to-position-btn" disabled>▶▶ 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();