commit eefb14484d957f6b48c6ca6cb9e831c292b23af7
parent d045b06f07d355f810b35a1ba018e53d3a442ad5
Author: ea <ea@ea.contact>
Date: Sun, 24 May 2026 15:43:01 +0000
techan stuff
Diffstat:
| M | frontend/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)