trtr

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

commit eefb14484d957f6b48c6ca6cb9e831c292b23af7
parent d045b06f07d355f810b35a1ba018e53d3a442ad5
Author: ea <ea@ea.contact>
Date:   Sun, 24 May 2026 15:43:01 +0000

techan stuff

Diffstat:
Mfrontend/index.html | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
1 file changed, 106 insertions(+), 46 deletions(-)

diff --git a/frontend/index.html b/frontend/index.html @@ -624,6 +624,14 @@ <div class="gym-section"> <h2>Active Prediction</h2> + <div class="fee-row" style="margin-bottom:4px"> + <label style="font-size:0.72rem;color:#808090">Fee</label> + <span style="display:flex;align-items:center;gap:3px"> + <input type="number" id="gym-fee-input" class="fee-input" + value="0.10" min="0" max="100" step="0.01"> + <span style="font-size:0.78rem;color:#606080">%</span> + </span> + </div> <div id="prediction-info">No prediction set</div> <button class="gym-btn danger" id="clear-prediction-btn">Cancel prediction</button> </div> @@ -941,6 +949,7 @@ document.getElementById('loading-overlay').style.display = 'flex'; if (candleSeries) { clearTradeLines(); clearCurrentBreakEven(); } gymPrediction = null; + gymClosedPredictions.length = 0; gymScore = 0; if (gymMode) { updatePredictionInfo(); updateGymScore(); } @@ -1108,6 +1117,7 @@ let gymActiveTool = null; // null = no tool, chart pans/zooms freely const gymDrawings = []; let gymPrediction = null; // { logTarget, entryPrice, direction: 'long'|'short' } + const gymClosedPredictions = []; // { logTarget, entryPrice, closePrice, direction, profit } let gymScore = 0; let gymIsDrawing = false; let gymCurrentPoints = []; @@ -1252,67 +1262,117 @@ function resolveGymPrediction(closePrice) { if (!gymPrediction) return; - const { entryPrice, direction } = gymPrediction; - const pct = direction === 'long' + const { logTarget, entryPrice, direction } = gymPrediction; + const feePct = parseFloat(document.getElementById('gym-fee-input').value) || 0; + const f = feePct / 100; + // fees applied twice (open + close), compounded + const feeFactor = (1 - f) * (1 - f); + const grossPct = direction === 'long' ? (closePrice - entryPrice) / entryPrice : (entryPrice - closePrice) / entryPrice; - gymScore += pct * 1000; + const profit = grossPct * feeFactor * 1000; + if (gymClosedPredictions.length >= 10) gymClosedPredictions.shift(); + gymClosedPredictions.push({ logTarget, entryPrice, closePrice, direction, profit }); + gymScore += profit; gymPrediction = null; updatePredictionInfo(); updateGymScore(); } + function drawPredictionMarker(ctx, pred, profit) { + const { logTarget, entryPrice, direction } = pred; + const tx = chart.timeScale().logicalToCoordinate(logTarget); + const ey = candleSeries.priceToCoordinate(entryPrice); + const col = direction === 'long' ? '#26a69a' : '#ef5350'; + const isClosed = profit !== null; + + if (tx == null || ey == null) return; + + // Vertical line at target bar + ctx.strokeStyle = col; + ctx.lineWidth = 1.5; + ctx.setLineDash([5, 4]); + ctx.globalAlpha = isClosed ? 0.35 : 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; + + // Close price line (closed predictions only) + if (isClosed && pred.closePrice != null) { + const cy = candleSeries.priceToCoordinate(pred.closePrice); + if (cy != null) { + const closedCol = profit >= 0 ? '#26a69a' : '#ef5350'; + ctx.strokeStyle = closedCol; + ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); + ctx.globalAlpha = 0.4; + ctx.beginPath(); + ctx.moveTo(0, cy); + ctx.lineTo(gymCanvas.width, cy); + ctx.stroke(); + ctx.setLineDash([]); + ctx.globalAlpha = 1; + } + } + + // Triangle arrow at entry-line / target intersection + const arrowDir = direction === 'long' ? -1 : 1; + const aw = 9, ah = 14; + ctx.fillStyle = col; + ctx.globalAlpha = isClosed ? 0.5 : 1; + 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(); + ctx.globalAlpha = 1; + + // Labels + const dirLabel = direction === 'long' ? 'LONG' : 'SHORT'; + ctx.fillStyle = col; + ctx.font = `bold 11px "Segoe UI", system-ui, sans-serif`; + ctx.fillText(dirLabel, 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); + + // Result badge on closed predictions + if (isClosed) { + const resultCol = profit >= 0 ? '#26a69a' : '#ef5350'; + const resultText = (profit >= 0 ? '+' : '') + '$' + fmt(profit); + ctx.font = 'bold 11px "Segoe UI", system-ui, sans-serif'; + const tw = ctx.measureText(resultText).width; + ctx.fillStyle = 'rgba(15,15,26,0.75)'; + ctx.fillRect(tx + 10, ey + 18, tw + 8, 17); + ctx.fillStyle = resultCol; + ctx.fillText(resultText, tx + 14, ey + 31); + } + } + function gymDrawFrame() { requestAnimationFrame(gymDrawFrame); const ctx = gymCtx; ctx.clearRect(0, 0, gymCanvas.width, gymCanvas.height); if (!gymMode) return; - // Prediction marker + // Active 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; + drawPredictionMarker(ctx, gymPrediction, null); + } - // 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); - } + // Closed prediction markers + for (const cp of gymClosedPredictions) { + drawPredictionMarker(ctx, cp, cp.profit); } // In-progress preview (line/pencil drag)