commit d045b06f07d355f810b35a1ba018e53d3a442ad5
parent d5b1593b4d2cee8e967368fffd26a4669ce651f0
Author: ea <ea@ea.contact>
Date: Sun, 24 May 2026 15:30:21 +0000
Add techalyzis gym mod
Diffstat:
| M | frontend/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…</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>▶▶ 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: $${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>