commit e30f39b127c1f0aca4ddfc842c2e86e6cc73cca5
parent eefb14484d957f6b48c6ca6cb9e831c292b23af7
Author: ea <ea@ea.contact>
Date: Sun, 24 May 2026 20:05:37 +0000
Techa: add play till close
Diffstat:
1 file changed, 53 insertions(+), 0 deletions(-)
diff --git a/frontend/index.html b/frontend/index.html
@@ -470,6 +470,9 @@
}
.gym-btn:hover { background: #2a2a4a; color: #a0a0c0; }
.gym-btn.danger:hover { background: #3a1a1a; color: #ef5350; border-color: #5a2020; }
+ .gym-btn.go { border-color: #1a4a3a; color: #26a69a; }
+ .gym-btn.go:hover:not(:disabled) { background: #0f2e26; color: #4fd4c0; border-color: #267a6a; }
+ .gym-btn:disabled, .gym-btn.go:disabled { opacity: 0.35; cursor: not-allowed; }
#text-input-overlay {
position: fixed;
@@ -633,6 +636,7 @@
</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>
@@ -1114,6 +1118,7 @@
// ── Gym Mode ──────────────────────────────────────────────────────────────
let gymMode = false;
+ let gymFastForwarding = false;
let gymActiveTool = null; // null = no tool, chart pans/zooms freely
const gymDrawings = [];
let gymPrediction = null; // { logTarget, entryPrice, direction: 'long'|'short' }
@@ -1237,9 +1242,11 @@
function updatePredictionInfo() {
const el = document.getElementById('prediction-info');
+ const ffBtn = document.getElementById('ff-to-position-btn');
if (!gymPrediction) {
el.textContent = 'No prediction set';
el.classList.remove('has-pred');
+ if (ffBtn) ffBtn.disabled = true;
return;
}
const { logTarget, entryPrice, direction } = gymPrediction;
@@ -1251,6 +1258,7 @@
`<span style="color:${col};font-weight:700">${dir}</span><br>` +
`Entry: $${fmt(entryPrice)}<br>` +
`Closes: ${barsLeft === 0 ? 'next bar' : '+' + barsLeft + ' bars'}`;
+ if (ffBtn) ffBtn.disabled = gymFastForwarding || barsLeft === 0;
}
function updateGymScore() {
@@ -1506,6 +1514,51 @@
updatePredictionInfo();
});
+ async function gymFastForwardToClose() {
+ if (!gymPrediction || gymFastForwarding) return;
+ gymFastForwarding = true;
+ document.getElementById('ff-to-position-btn').disabled = true;
+ document.getElementById('next-btn').disabled = true;
+
+ while (gymPrediction && gymLastLogical < gymPrediction.logTarget) {
+ 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;
+ break;
+ }
+
+ 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 (gymPrediction && gymLastLogical >= gymPrediction.logTarget) {
+ resolveGymPrediction(c.close);
+ break;
+ } else {
+ updatePredictionInfo();
+ }
+
+ await new Promise(r => setTimeout(r, 120));
+ }
+
+ gymFastForwarding = false;
+ document.getElementById('next-btn').disabled = false;
+ updatePredictionInfo();
+ }
+
+ document.getElementById('ff-to-position-btn').addEventListener('click', gymFastForwardToClose);
+
document.getElementById('clear-drawings-btn').addEventListener('click', () => {
gymDrawings.length = 0;
});