trtr

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

index.html (37148B)


      1 <!DOCTYPE html>
      2 <html lang="en">
      3 <head>
      4 <meta charset="UTF-8">
      5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
      6 <title>BTC/USD Simulator</title>
      7 <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
      8 <style>
      9   * { box-sizing: border-box; margin: 0; padding: 0; }
     10 
     11   body {
     12     background: #0f0f1a;
     13     color: #e0e0e0;
     14     font-family: 'Segoe UI', system-ui, sans-serif;
     15     height: 100vh;
     16     display: flex;
     17     flex-direction: column;
     18   }
     19 
     20   header {
     21     display: flex;
     22     align-items: center;
     23     justify-content: space-between;
     24     padding: 10px 18px;
     25     background: #16162a;
     26     border-bottom: 1px solid #2a2a4a;
     27     flex-shrink: 0;
     28   }
     29 
     30   header h1 {
     31     font-size: 1rem;
     32     font-weight: 600;
     33     color: #f0b429;
     34     letter-spacing: 0.5px;
     35   }
     36 
     37   .header-controls {
     38     display: flex;
     39     align-items: center;
     40     gap: 6px;
     41   }
     42 
     43   .period-select {
     44     background: #1e1e36;
     45     color: #a0a0c0;
     46     border: 1px solid #3a3a5a;
     47     border-radius: 6px;
     48     padding: 5px 8px;
     49     font-size: 0.8rem;
     50     cursor: pointer;
     51   }
     52   .period-select:focus { outline: none; border-color: #f0b429; }
     53   .period-select option { background: #1e1e36; }
     54 
     55   #new-session-btn {
     56     background: #2a2a4a;
     57     color: #a0a0c0;
     58     border: 1px solid #3a3a5a;
     59     border-radius: 6px;
     60     padding: 6px 14px;
     61     font-size: 0.8rem;
     62     cursor: pointer;
     63     transition: background 0.15s;
     64   }
     65   #new-session-btn:hover { background: #3a3a5a; color: #e0e0e0; }
     66 
     67   #help-btn {
     68     width: 28px;
     69     height: 28px;
     70     background: #1e1e36;
     71     color: #606080;
     72     border: 1px solid #3a3a5a;
     73     border-radius: 6px;
     74     font-size: 0.85rem;
     75     cursor: pointer;
     76     display: flex;
     77     align-items: center;
     78     justify-content: center;
     79     flex-shrink: 0;
     80     transition: background 0.15s, color 0.15s;
     81   }
     82   #help-btn:hover { background: #2a2a4a; color: #c0c0e0; }
     83 
     84   #help-overlay {
     85     position: fixed;
     86     inset: 0;
     87     background: rgba(0,0,0,0.6);
     88     display: flex;
     89     align-items: center;
     90     justify-content: center;
     91     z-index: 500;
     92     padding: 16px;
     93   }
     94   #help-overlay.hidden { display: none; }
     95 
     96   #help-box {
     97     background: #16162a;
     98     border: 1px solid #2a2a4a;
     99     border-radius: 10px;
    100     padding: 24px 28px;
    101     max-width: 480px;
    102     width: 100%;
    103     max-height: 90vh;
    104     overflow-y: auto;
    105     position: relative;
    106     font-size: 0.85rem;
    107     line-height: 1.65;
    108     color: #c0c0d8;
    109   }
    110   #help-box h2 {
    111     font-size: 0.95rem;
    112     font-weight: 600;
    113     color: #f0b429;
    114     margin-bottom: 8px;
    115   }
    116   #help-box h2:not(:first-child) { margin-top: 20px; }
    117   #help-box p { margin-bottom: 0; }
    118   #help-box ol { padding-left: 18px; display: flex; flex-direction: column; gap: 6px; }
    119   #help-box .help-note {
    120     margin-top: 20px;
    121     padding: 10px 14px;
    122     background: #1e1e36;
    123     border: 1px solid #3a3a5a;
    124     border-radius: 6px;
    125     font-size: 0.78rem;
    126     color: #808098;
    127     line-height: 1.6;
    128   }
    129   #help-close {
    130     position: absolute;
    131     top: 12px;
    132     right: 14px;
    133     background: none;
    134     border: none;
    135     color: #505070;
    136     font-size: 1.1rem;
    137     cursor: pointer;
    138     line-height: 1;
    139     padding: 2px 4px;
    140   }
    141   #help-close:hover { color: #c0c0e0; }
    142 
    143   .pnl-positive { color: #26a69a; }
    144   .pnl-negative { color: #ef5350; }
    145   .pnl-zero     { color: #e0e0e0; }
    146 
    147   .main {
    148     display: flex;
    149     flex: 1;
    150     overflow: hidden;
    151   }
    152 
    153   #chart-container {
    154     flex: 1;
    155     min-width: 0;
    156     position: relative;
    157   }
    158 
    159   #chart {
    160     width: 100%;
    161     height: 100%;
    162   }
    163 
    164   #loading-overlay {
    165     position: absolute;
    166     inset: 0;
    167     background: #0f0f1a;
    168     display: flex;
    169     align-items: center;
    170     justify-content: center;
    171     font-size: 1rem;
    172     color: #606080;
    173     z-index: 10;
    174   }
    175 
    176   .fee-row {
    177     display: flex;
    178     align-items: center;
    179     justify-content: space-between;
    180     gap: 8px;
    181   }
    182 
    183   .fee-row label {
    184     font-size: 0.72rem;
    185     color: #808090;
    186     white-space: nowrap;
    187   }
    188 
    189   .fee-input {
    190     width: 68px;
    191     background: #1e1e36;
    192     border: 1px solid #3a3a5a;
    193     border-radius: 4px;
    194     color: #e0e0e0;
    195     font-size: 0.82rem;
    196     padding: 3px 6px;
    197     text-align: right;
    198   }
    199   .fee-input:focus { outline: none; border-color: #f0b429; }
    200 
    201   footer {
    202     padding: 10px 18px;
    203     background: #16162a;
    204     border-top: 1px solid #2a2a4a;
    205     display: flex;
    206     justify-content: center;
    207     align-items: center;
    208     gap: 8px;
    209     flex-shrink: 0;
    210   }
    211 
    212   #next-btn {
    213     background: #2a2a6a;
    214     color: #a0a0e0;
    215     border: 1px solid #4a4a8a;
    216     border-radius: 6px;
    217     padding: 9px 36px;
    218     font-size: 0.9rem;
    219     font-weight: 600;
    220     cursor: pointer;
    221     transition: background 0.15s;
    222   }
    223   #next-btn:hover:not(:disabled) { background: #3a3a8a; color: #e0e0ff; }
    224   #next-btn:disabled { opacity: 0.4; cursor: not-allowed; }
    225 
    226   #auto-play-btn {
    227     background: #2a2a6a;
    228     color: #a0a0e0;
    229     border: 1px solid #4a4a8a;
    230     border-radius: 6px;
    231     width: 36px;
    232     height: 36px;
    233     font-size: 1rem;
    234     cursor: pointer;
    235     transition: background 0.15s, color 0.15s;
    236     display: flex;
    237     align-items: center;
    238     justify-content: center;
    239     flex-shrink: 0;
    240   }
    241   #auto-play-btn:hover:not(:disabled) { background: #3a3a8a; color: #e0e0ff; }
    242   #auto-play-btn:disabled { opacity: 0.4; cursor: not-allowed; }
    243   #auto-play-btn.playing { background: #3a2a6a; color: #d0a0ff; border-color: #7a4aaa; }
    244 
    245   #ff-to-position-btn {
    246     background: #0f2e26;
    247     color: #26a69a;
    248     border: 1px solid #1a4a3a;
    249     border-radius: 6px;
    250     padding: 9px 20px;
    251     font-size: 0.9rem;
    252     font-weight: 600;
    253     cursor: pointer;
    254     transition: background 0.15s, color 0.15s;
    255   }
    256   #ff-to-position-btn:hover:not(:disabled) { background: #143d32; color: #4fd4c0; border-color: #267a6a; }
    257   #ff-to-position-btn:disabled { opacity: 0.4; cursor: not-allowed; }
    258 
    259   .footer-btn-group {
    260     display: flex;
    261     flex-direction: column;
    262     align-items: center;
    263     gap: 2px;
    264   }
    265 
    266   .key-hint {
    267     font-size: 0.6rem;
    268     color: #383858;
    269     letter-spacing: 0.5px;
    270   }
    271 
    272   /* ── Gym Mode ────────────────────────────────────────────────────────── */
    273 
    274 
    275 
    276   #gym-canvas {
    277     position: absolute;
    278     inset: 0;
    279     width: 100%;
    280     height: 100%;
    281     pointer-events: none;
    282     z-index: 2;
    283   }
    284 
    285   #gym-sidebar {
    286     display: flex;
    287     width: 220px;
    288     flex-shrink: 0;
    289     background: #16162a;
    290     border-left: 1px solid #2a2a4a;
    291     flex-direction: column;
    292     padding: 16px;
    293     gap: 18px;
    294     overflow-y: auto;
    295   }
    296 
    297   .gym-section { display: flex; flex-direction: column; gap: 8px; }
    298 
    299   .gym-section h2 {
    300     font-size: 0.7rem;
    301     text-transform: uppercase;
    302     letter-spacing: 1px;
    303     color: #505070;
    304     margin-bottom: 2px;
    305   }
    306 
    307   .gym-tool-grid {
    308     display: grid;
    309     grid-template-columns: 1fr 1fr;
    310     gap: 5px;
    311   }
    312 
    313   .gym-tool-btn {
    314     background: #1e1e36;
    315     color: #606080;
    316     border: 1px solid #3a3a5a;
    317     border-radius: 5px;
    318     padding: 7px 4px;
    319     font-size: 0.75rem;
    320     cursor: pointer;
    321     transition: background 0.1s, color 0.1s;
    322     text-align: center;
    323   }
    324   .gym-tool-btn:hover { background: #2a2a4a; color: #a0a0c0; }
    325   .gym-tool-btn.active {
    326     background: #28105a;
    327     color: #c0a0ff;
    328     border-color: #5030a0;
    329   }
    330 
    331   .gym-hint {
    332     font-size: 0.7rem;
    333     color: #404060;
    334     line-height: 1.45;
    335     min-height: 2.2em;
    336   }
    337 
    338   #prediction-info {
    339     background: #1a1a30;
    340     border: 1px solid #3a3a5a;
    341     border-radius: 5px;
    342     padding: 8px;
    343     font-size: 0.72rem;
    344     color: #505070;
    345     line-height: 1.7;
    346     min-height: 64px;
    347     font-variant-numeric: tabular-nums;
    348   }
    349   #prediction-info.has-pred { border-color: #5030a0; color: #c0a0ff; }
    350 
    351   .gym-btn {
    352     width: 100%;
    353     background: #1e1e36;
    354     border: 1px solid #3a3a5a;
    355     border-radius: 5px;
    356     color: #505070;
    357     font-size: 0.75rem;
    358     padding: 6px 0;
    359     cursor: pointer;
    360     transition: background 0.1s, color 0.1s;
    361   }
    362   .gym-btn:hover { background: #2a2a4a; color: #a0a0c0; }
    363   .gym-btn.danger:hover { background: #3a1a1a; color: #ef5350; border-color: #5a2020; }
    364   .gym-btn.go { border-color: #1a4a3a; color: #26a69a; }
    365   .gym-btn.go:hover:not(:disabled) { background: #0f2e26; color: #4fd4c0; border-color: #267a6a; }
    366   .gym-btn:disabled, .gym-btn.go:disabled { opacity: 0.35; cursor: not-allowed; }
    367 
    368   #text-input-overlay {
    369     position: fixed;
    370     background: #1e1e36;
    371     border: 1px solid #5030a0;
    372     border-radius: 4px;
    373     padding: 3px 7px;
    374     color: #e0d8ff;
    375     font-size: 0.82rem;
    376     z-index: 200;
    377     outline: none;
    378     min-width: 110px;
    379     display: none;
    380   }
    381 </style>
    382 </head>
    383 <body>
    384 
    385 <header>
    386   <h1>BTC/USD Simulator</h1>
    387   <div class="header-controls">
    388     <select id="year-select" class="period-select">
    389       <option value="">Any year</option>
    390       <option value="2012">2012</option>
    391       <option value="2013">2013</option>
    392       <option value="2014">2014</option>
    393       <option value="2015">2015</option>
    394       <option value="2016">2016</option>
    395       <option value="2017">2017</option>
    396       <option value="2018">2018</option>
    397       <option value="2019">2019</option>
    398       <option value="2020">2020</option>
    399       <option value="2021">2021</option>
    400       <option value="2022">2022</option>
    401       <option value="2023">2023</option>
    402       <option value="2024">2024</option>
    403       <option value="2025">2025</option>
    404       <option value="2026">2026</option>
    405     </select>
    406     <select id="month-select" class="period-select" disabled>
    407       <option value="">Any month</option>
    408       <option value="1">Jan</option>
    409       <option value="2">Feb</option>
    410       <option value="3">Mar</option>
    411       <option value="4">Apr</option>
    412       <option value="5">May</option>
    413       <option value="6">Jun</option>
    414       <option value="7">Jul</option>
    415       <option value="8">Aug</option>
    416       <option value="9">Sep</option>
    417       <option value="10">Oct</option>
    418       <option value="11">Nov</option>
    419       <option value="12">Dec</option>
    420     </select>
    421     <button id="new-session-btn">New Session</button>
    422     <button id="help-btn">?</button>
    423   </div>
    424 </header>
    425 
    426 <div class="main">
    427   <div id="chart-container">
    428     <div id="chart"></div>
    429     <canvas id="gym-canvas"></canvas>
    430     <div id="loading-overlay">Loading data&hellip;</div>
    431   </div>
    432 
    433   <div id="gym-sidebar">
    434     <div class="gym-section">
    435       <h2>Drawing Tools</h2>
    436       <div class="gym-tool-grid">
    437         <button class="gym-tool-btn" id="tool-line">Line</button>
    438         <button class="gym-tool-btn" id="tool-pencil">Pencil</button>
    439         <button class="gym-tool-btn" id="tool-text">Text</button>
    440         <button class="gym-tool-btn" id="tool-rect">Rectangle</button>
    441       </div>
    442       <p class="gym-hint" id="tool-hint">Select a tool or pan/zoom freely.</p>
    443     </div>
    444 
    445     <div class="gym-section">
    446       <h2>Active Prediction</h2>
    447       <div class="fee-row" style="margin-bottom:4px">
    448         <label style="font-size:0.72rem;color:#808090">Fee</label>
    449         <span style="display:flex;align-items:center;gap:3px">
    450           <input type="number" id="gym-fee-input" class="fee-input"
    451                  value="0.10" min="0" max="100" step="0.01">
    452           <span style="font-size:0.78rem;color:#606080">%</span>
    453         </span>
    454       </div>
    455       <div id="prediction-info">No prediction set</div>
    456       <button class="gym-btn danger" id="clear-prediction-btn">Cancel prediction</button>
    457     </div>
    458 
    459     <div class="gym-section">
    460       <h2>Score</h2>
    461       <div id="gym-score" class="pnl-zero" style="font-size:1.1rem;font-weight:700;font-variant-numeric:tabular-nums">$0.00</div>
    462     </div>
    463 
    464     <div class="gym-section" style="margin-top:auto">
    465       <button class="gym-btn danger" id="clear-drawings-btn">Clear all drawings</button>
    466     </div>
    467   </div>
    468 </div>
    469 
    470 <input id="text-input-overlay" type="text" placeholder="Type and press Enter">
    471 
    472 <footer>
    473   <div class="footer-btn-group">
    474     <button id="next-btn" disabled>&#9654;&#9654; Next Candle</button>
    475     <span class="key-hint">Space</span>
    476   </div>
    477   <div class="footer-btn-group">
    478     <button id="auto-play-btn" disabled title="Auto-play (~8 candles/sec)">&#9654;</button>
    479     <span class="key-hint">&nbsp;</span>
    480   </div>
    481   <div class="footer-btn-group" id="ff-btn-group">
    482     <button id="ff-to-position-btn" disabled>&#9654;&#9654; Play to close</button>
    483     <span class="key-hint">C</span>
    484   </div>
    485 </footer>
    486 
    487 <div id="help-overlay" class="hidden">
    488   <div id="help-box">
    489     <button id="help-close">&#x2715;</button>
    490     <h2>What is this?</h2>
    491     <p>You are looking at real historic 1 minute BTC/USD pair trading data from Bitstamp. Try predicting how it is going to behave in the next minutes! Some basic tools for visual technical analysis are at your disposal.</p>
    492     <h2>How to play?</h2>
    493     <ol>
    494       <li>Double click somewhere on the right from the last candle to place a prediction. Clicking above the current price creates a long prediction. Create a short prediction by clicking below the current price.</li>
    495       <li>Let the time progress till execution by adding candles with the buttons on the bottom.</li>
    496       <li>&hellip;</li>
    497       <li>Profit!! (or not)</li>
    498     </ol>
    499     <div class="help-note">
    500       <strong style="color:#a0a0c0">Note</strong><br>
    501       A long position counts as a positive x1 buy-sell of 100 USD. Equivalent to buying BTC at the current price and selling at the price at the set moment of the prediction. Short is the same but in reverse.
    502     </div>
    503   </div>
    504 </div>
    505 
    506 <script>
    507   let sessionId = null;
    508   let chart = null;
    509   let candleSeries = null;
    510   let currentPrice = 0;
    511   let gymLastLogical = 0;
    512 
    513   // ── Chart init ────────────────────────────────────────────────────────────
    514 
    515   function initChart() {
    516     const container = document.getElementById('chart');
    517     chart = LightweightCharts.createChart(container, {
    518       width: container.clientWidth,
    519       height: container.clientHeight,
    520       layout: {
    521         background: { color: '#0f0f1a' },
    522         textColor: '#a0a0b0',
    523       },
    524       grid: {
    525         vertLines: { color: '#1e1e36' },
    526         horzLines: { color: '#1e1e36' },
    527       },
    528       crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
    529       rightPriceScale: { borderColor: '#2a2a4a' },
    530       timeScale: {
    531         borderColor: '#2a2a4a',
    532         timeVisible: true,
    533         secondsVisible: false,
    534       },
    535     });
    536 
    537     candleSeries = chart.addCandlestickSeries({
    538       upColor: '#26a69a',
    539       downColor: '#ef5350',
    540       borderVisible: false,
    541       wickUpColor: '#26a69a',
    542       wickDownColor: '#ef5350',
    543     });
    544 
    545     window.addEventListener('resize', () => {
    546       chart.applyOptions({
    547         width: container.clientWidth,
    548         height: container.clientHeight,
    549       });
    550     });
    551   }
    552 
    553   // ── Session ───────────────────────────────────────────────────────────────
    554 
    555   document.getElementById('year-select').addEventListener('change', e => {
    556     const monthSel = document.getElementById('month-select');
    557     monthSel.disabled = !e.target.value;
    558     if (!e.target.value) monthSel.value = '';
    559   });
    560 
    561   async function startSession() {
    562     stopAutoPlay();
    563     setEnabled(false);
    564     document.getElementById('loading-overlay').style.display = 'flex';
    565     gymActivePrediction = null;
    566     gymResolvedPredictions.length = 0;
    567     gymDrawings.length = 0;
    568     gymScore = 0;
    569     updatePredictionInfo();
    570     updateGymScore();
    571 
    572     const year  = document.getElementById('year-select').value;
    573     const month = document.getElementById('month-select').value;
    574     const body = {};
    575     if (year)  body.year  = parseInt(year,  10);
    576     if (month) body.month = parseInt(month, 10);
    577 
    578     try {
    579       const res = await fetch('/api/session/new', {
    580         method: 'POST',
    581         headers: { 'Content-Type': 'application/json' },
    582         body: JSON.stringify(body),
    583       });
    584       const data = await res.json();
    585       sessionId = data.session_id;
    586       localStorage.setItem('btc_sim_session', sessionId);
    587 
    588       const lwcCandles = data.candles.map(c => ({
    589         time: c.ts,
    590         open: c.open,
    591         high: c.high,
    592         low: c.low,
    593         close: c.close,
    594       }));
    595       candleSeries.setData(lwcCandles);
    596       chart.timeScale().fitContent();
    597       gymLastLogical = lwcCandles.length - 1;
    598 
    599       currentPrice = data.candles[data.candles.length - 1].close;
    600       setEnabled(true);
    601     } catch (err) {
    602       document.getElementById('loading-overlay').textContent = 'Failed to connect to server.';
    603       return;
    604     }
    605 
    606     document.getElementById('loading-overlay').style.display = 'none';
    607   }
    608 
    609   // ── Helpers ───────────────────────────────────────────────────────────────
    610 
    611   function fmt(n, decimals = 2) {
    612     return n.toLocaleString('en-US', {
    613       minimumFractionDigits: decimals,
    614       maximumFractionDigits: decimals,
    615     });
    616   }
    617 
    618   function setEnabled(enabled) {
    619     document.getElementById('next-btn').disabled = !enabled;
    620     document.getElementById('auto-play-btn').disabled = !enabled;
    621   }
    622 
    623   // ── Next candle ───────────────────────────────────────────────────────────
    624 
    625   document.getElementById('next-btn').addEventListener('click', async () => {
    626     const res = await fetch('/api/next', {
    627       method: 'POST',
    628       headers: { 'Content-Type': 'application/json' },
    629       body: JSON.stringify({ session_id: sessionId }),
    630     });
    631 
    632     if (res.status === 409) {
    633       document.getElementById('next-btn').textContent = 'End of data';
    634       document.getElementById('next-btn').disabled = true;
    635       return;
    636     }
    637 
    638     const data = await res.json();
    639     const c = data.candle;
    640     candleSeries.update({ time: c.ts, open: c.open, high: c.high, low: c.low, close: c.close });
    641     chart.timeScale().scrollToRealTime();
    642     gymLastLogical++;
    643     currentPrice = c.close;
    644     if (data.resolved) handleResolvedPrediction(data.resolved);
    645     else updatePredictionInfo();
    646   });
    647 
    648   // ── Auto-play ─────────────────────────────────────────────────────────────
    649 
    650   let autoPlayActive = false;
    651   let autoPlayTimer = null;
    652 
    653   function stopAutoPlay() {
    654     autoPlayActive = false;
    655     clearTimeout(autoPlayTimer);
    656     autoPlayTimer = null;
    657     const btn = document.getElementById('auto-play-btn');
    658     btn.textContent = '►';
    659     btn.title = 'Auto-play (~8 candles/sec)';
    660     btn.classList.remove('playing');
    661   }
    662 
    663   async function autoPlayStep() {
    664     if (!autoPlayActive) return;
    665 
    666     const res = await fetch('/api/next', {
    667       method: 'POST',
    668       headers: { 'Content-Type': 'application/json' },
    669       body: JSON.stringify({ session_id: sessionId }),
    670     });
    671 
    672     if (res.status === 409) {
    673       document.getElementById('next-btn').textContent = 'End of data';
    674       document.getElementById('next-btn').disabled = true;
    675       stopAutoPlay();
    676       return;
    677     }
    678 
    679     const data = await res.json();
    680     const c = data.candle;
    681     candleSeries.update({ time: c.ts, open: c.open, high: c.high, low: c.low, close: c.close });
    682     chart.timeScale().scrollToRealTime();
    683     gymLastLogical++;
    684     currentPrice = c.close;
    685     if (data.resolved) handleResolvedPrediction(data.resolved);
    686     else updatePredictionInfo();
    687 
    688     if (autoPlayActive) {
    689       autoPlayTimer = setTimeout(autoPlayStep, 120);
    690     }
    691   }
    692 
    693   function toggleAutoPlay() {
    694     if (autoPlayActive) {
    695       stopAutoPlay();
    696     } else {
    697       autoPlayActive = true;
    698       const btn = document.getElementById('auto-play-btn');
    699       btn.textContent = '■';
    700       btn.title = 'Stop auto-play';
    701       btn.classList.add('playing');
    702       autoPlayStep();
    703     }
    704   }
    705 
    706   document.getElementById('auto-play-btn').addEventListener('click', toggleAutoPlay);
    707 
    708   // ── New session button ────────────────────────────────────────────────────
    709 
    710   document.getElementById('new-session-btn').addEventListener('click', startSession);
    711 
    712   // ── Help overlay ──────────────────────────────────────────────────────────
    713 
    714   const helpOverlay = document.getElementById('help-overlay');
    715   document.getElementById('help-btn').addEventListener('click', () => helpOverlay.classList.remove('hidden'));
    716   document.getElementById('help-close').addEventListener('click', () => helpOverlay.classList.add('hidden'));
    717   helpOverlay.addEventListener('click', e => { if (e.target === helpOverlay) helpOverlay.classList.add('hidden'); });
    718   document.addEventListener('keydown', e => { if (e.key === 'Escape') helpOverlay.classList.add('hidden'); });
    719 
    720   // ── Gym Mode ──────────────────────────────────────────────────────────────
    721 
    722   let gymMode = true;
    723   let gymFastForwarding = false;
    724   let gymActiveTool = null;
    725   const gymDrawings = [];
    726   let gymActivePrediction = null;  // { logTarget, entryPrice, direction }
    727   const gymResolvedPredictions = [];
    728   let gymScore = 0;
    729   let gymIsDrawing = false;
    730   let gymCurrentPoints = [];
    731   let gymTextPending = null;
    732   let gymHoverPos = null;
    733 
    734   const gymCanvas = document.getElementById('gym-canvas');
    735   const gymCtx = gymCanvas.getContext('2d');
    736   const textInputEl = document.getElementById('text-input-overlay');
    737 
    738   function resizeGymCanvas() {
    739     const r = document.getElementById('chart-container').getBoundingClientRect();
    740     gymCanvas.width = r.width;
    741     gymCanvas.height = r.height;
    742   }
    743 
    744   window.addEventListener('resize', resizeGymCanvas);
    745 
    746   function gymPos(e) {
    747     const r = gymCanvas.getBoundingClientRect();
    748     return { x: e.clientX - r.left, y: e.clientY - r.top };
    749   }
    750 
    751   function pxToChart(x, y) {
    752     return {
    753       logical: chart.timeScale().coordinateToLogical(x),
    754       price: candleSeries.coordinateToPrice(y),
    755     };
    756   }
    757 
    758   function chartToPx(logical, price) {
    759     return {
    760       x: chart.timeScale().logicalToCoordinate(logical),
    761       y: candleSeries.priceToCoordinate(price),
    762     };
    763   }
    764 
    765   gymCanvas.addEventListener('mousedown', e => {
    766     if (!gymMode) return;
    767     const { x, y } = gymPos(e);
    768     const { logical, price } = pxToChart(x, y);
    769     if (logical == null || price == null) return;
    770     gymIsDrawing = true;
    771     gymCurrentPoints = [{ logical, price }];
    772   });
    773 
    774   gymCanvas.addEventListener('mousemove', e => {
    775     if (!gymMode) return;
    776     const { x, y } = gymPos(e);
    777     const { logical, price } = pxToChart(x, y);
    778     gymHoverPos = (logical != null && price != null) ? { logical, price } : null;
    779     if (!gymIsDrawing) return;
    780     if (logical == null || price == null) return;
    781     if (gymActiveTool === 'pencil') {
    782       gymCurrentPoints.push({ logical, price });
    783     } else {
    784       gymCurrentPoints[1] = { logical, price };
    785     }
    786   });
    787 
    788   gymCanvas.addEventListener('mouseup', e => {
    789     if (!gymMode || !gymIsDrawing) return;
    790     gymIsDrawing = false;
    791     const { x, y } = gymPos(e);
    792     const { logical, price } = pxToChart(x, y);
    793 
    794     if (gymActiveTool === 'line' && gymCurrentPoints.length >= 2) {
    795       gymDrawings.push({ type: 'line', points: [...gymCurrentPoints] });
    796     } else if (gymActiveTool === 'pencil' && gymCurrentPoints.length >= 2) {
    797       gymDrawings.push({ type: 'pencil', points: [...gymCurrentPoints] });
    798     } else if (gymActiveTool === 'text') {
    799       if (logical != null && price != null) {
    800         gymTextPending = { logical, price };
    801         const cr = gymCanvas.getBoundingClientRect();
    802         textInputEl.style.left = (cr.left + x + 4) + 'px';
    803         textInputEl.style.top  = (cr.top  + y - 14) + 'px';
    804         textInputEl.style.display = 'block';
    805         textInputEl.value = '';
    806         textInputEl.focus();
    807       }
    808     } else if (gymActiveTool === 'rect' && gymCurrentPoints.length >= 2) {
    809       gymDrawings.push({ type: 'rect', points: [...gymCurrentPoints] });
    810     }
    811     gymCurrentPoints = [];
    812   });
    813 
    814   gymCanvas.addEventListener('mouseleave', () => {
    815     gymHoverPos = null;
    816     if (gymIsDrawing && gymActiveTool === 'pencil' && gymCurrentPoints.length >= 2) {
    817       gymDrawings.push({ type: 'pencil', points: [...gymCurrentPoints] });
    818     }
    819     gymIsDrawing = false;
    820     gymCurrentPoints = [];
    821   });
    822 
    823   textInputEl.addEventListener('keydown', e => {
    824     if (e.key === 'Enter') {
    825       const text = textInputEl.value.trim();
    826       if (text && gymTextPending) {
    827         gymDrawings.push({ type: 'text', points: [{ ...gymTextPending }], text });
    828       }
    829       textInputEl.style.display = 'none';
    830       gymTextPending = null;
    831     } else if (e.key === 'Escape') {
    832       textInputEl.style.display = 'none';
    833       gymTextPending = null;
    834     }
    835   });
    836 
    837   function updatePredictionInfo() {
    838     const el = document.getElementById('prediction-info');
    839     const ffBtn = document.getElementById('ff-to-position-btn');
    840     if (!gymActivePrediction) {
    841       el.textContent = 'No prediction set';
    842       el.classList.remove('has-pred');
    843       if (ffBtn) ffBtn.disabled = true;
    844       return;
    845     }
    846     const { logTarget, entryPrice, direction } = gymActivePrediction;
    847     const barsLeft = Math.max(0, logTarget - gymLastLogical);
    848     const dir = direction === 'long' ? '▲ LONG' : '▼ SHORT';
    849     const col = direction === 'long' ? '#26a69a' : '#ef5350';
    850     el.classList.add('has-pred');
    851     el.innerHTML =
    852       `<span style="color:${col};font-weight:700">${dir}</span><br>` +
    853       `Entry:&nbsp; $${fmt(entryPrice)}<br>` +
    854       `Closes: ${barsLeft === 0 ? 'next bar' : '+' + barsLeft + ' bars'}`;
    855     if (ffBtn) ffBtn.disabled = gymFastForwarding || barsLeft === 0;
    856   }
    857 
    858   function updateGymScore() {
    859     const el = document.getElementById('gym-score');
    860     el.textContent = (gymScore >= 0 ? '+' : '') + '$' + fmt(gymScore);
    861     el.className = gymScore > 0 ? 'pnl-positive' : gymScore < 0 ? 'pnl-negative' : 'pnl-zero';
    862     el.style.cssText += ';font-size:1.1rem;font-weight:700;font-variant-numeric:tabular-nums';
    863   }
    864 
    865   function handleResolvedPrediction(resolved) {
    866     const logTarget = gymActivePrediction ? gymActivePrediction.logTarget : gymLastLogical;
    867     if (gymResolvedPredictions.length >= 10) gymResolvedPredictions.shift();
    868     gymResolvedPredictions.push({
    869       logTarget,
    870       entryPrice: resolved.entry_price,
    871       closePrice: resolved.close_price,
    872       direction:  resolved.direction,
    873       profit:     resolved.profit,
    874     });
    875     gymScore += resolved.profit;
    876     gymActivePrediction = null;
    877     updatePredictionInfo();
    878     updateGymScore();
    879   }
    880 
    881   function drawPredictionMarker(ctx, pred, profit) {
    882     const { logTarget, entryPrice, direction } = pred;
    883     const tx = chart.timeScale().logicalToCoordinate(logTarget);
    884     const ey = candleSeries.priceToCoordinate(entryPrice);
    885     const col = direction === 'long' ? '#26a69a' : '#ef5350';
    886     const isClosed = profit !== null;
    887 
    888     if (tx == null || ey == null) return;
    889 
    890     // Vertical line at target bar
    891     ctx.strokeStyle = col;
    892     ctx.lineWidth = 1.5;
    893     ctx.setLineDash([5, 4]);
    894     ctx.globalAlpha = isClosed ? 0.35 : 0.55;
    895     ctx.beginPath();
    896     ctx.moveTo(tx, 0);
    897     ctx.lineTo(tx, gymCanvas.height);
    898     ctx.stroke();
    899     ctx.setLineDash([]);
    900 
    901     // Horizontal entry-price line
    902     ctx.beginPath();
    903     ctx.moveTo(0, ey);
    904     ctx.lineTo(gymCanvas.width, ey);
    905     ctx.stroke();
    906     ctx.globalAlpha = 1;
    907 
    908     // Close price line (closed predictions only)
    909     if (isClosed && pred.closePrice != null) {
    910       const cy = candleSeries.priceToCoordinate(pred.closePrice);
    911       if (cy != null) {
    912         const closedCol = profit >= 0 ? '#26a69a' : '#ef5350';
    913         ctx.strokeStyle = closedCol;
    914         ctx.lineWidth = 1;
    915         ctx.setLineDash([3, 3]);
    916         ctx.globalAlpha = 0.4;
    917         ctx.beginPath();
    918         ctx.moveTo(0, cy);
    919         ctx.lineTo(gymCanvas.width, cy);
    920         ctx.stroke();
    921         ctx.setLineDash([]);
    922         ctx.globalAlpha = 1;
    923       }
    924     }
    925 
    926     // Triangle arrow at entry-line / target intersection
    927     const arrowDir = direction === 'long' ? -1 : 1;
    928     const aw = 9, ah = 14;
    929     ctx.fillStyle = col;
    930     ctx.globalAlpha = isClosed ? 0.5 : 1;
    931     ctx.beginPath();
    932     ctx.moveTo(tx, ey + arrowDir * ah);
    933     ctx.lineTo(tx - aw, ey - arrowDir * 4);
    934     ctx.lineTo(tx + aw, ey - arrowDir * 4);
    935     ctx.closePath();
    936     ctx.fill();
    937     ctx.globalAlpha = 1;
    938 
    939     // Result badge on closed predictions
    940     if (isClosed) {
    941       const resultCol = profit >= 0 ? '#26a69a' : '#ef5350';
    942       const resultText = (profit >= 0 ? '+' : '') + '$' + fmt(profit);
    943       ctx.font = 'bold 11px "Segoe UI", system-ui, sans-serif';
    944       const tw = ctx.measureText(resultText).width;
    945       ctx.fillStyle = 'rgba(15,15,26,0.75)';
    946       ctx.fillRect(tx + 10, ey + 18, tw + 8, 17);
    947       ctx.fillStyle = resultCol;
    948       ctx.fillText(resultText, tx + 14, ey + 31);
    949     }
    950   }
    951 
    952   function gymDrawFrame() {
    953     requestAnimationFrame(gymDrawFrame);
    954     const ctx = gymCtx;
    955     ctx.clearRect(0, 0, gymCanvas.width, gymCanvas.height);
    956     if (!gymMode) return;
    957 
    958     // Active prediction marker
    959     if (gymActivePrediction) {
    960       drawPredictionMarker(ctx, gymActivePrediction, null);
    961     }
    962 
    963     // Resolved prediction markers
    964     for (const cp of gymResolvedPredictions) {
    965       drawPredictionMarker(ctx, cp, cp.profit);
    966     }
    967 
    968     // In-progress preview
    969     if (gymIsDrawing && gymCurrentPoints.length >= 1) {
    970       renderGymShape(ctx, gymActiveTool, gymCurrentPoints, null, 0.55);
    971     }
    972 
    973     // Saved drawings
    974     for (const d of gymDrawings) {
    975       renderGymShape(ctx, d.type, d.points, d.text, 1);
    976     }
    977   }
    978 
    979   function renderGymShape(ctx, type, points, text, alpha) {
    980     const px = points.map(p => chartToPx(p.logical, p.price));
    981     if (px.length === 0 || px[0].x == null) return;
    982     ctx.globalAlpha = alpha;
    983 
    984     if (type === 'line' || type === 'pencil') {
    985       ctx.strokeStyle = type === 'line' ? '#f0c040' : '#88aaff';
    986       ctx.lineWidth   = 1.8;
    987       ctx.lineJoin    = 'round';
    988       ctx.lineCap     = 'round';
    989       ctx.beginPath();
    990       ctx.moveTo(px[0].x, px[0].y);
    991       for (let i = 1; i < px.length; i++) {
    992         if (px[i].x != null && px[i].y != null) ctx.lineTo(px[i].x, px[i].y);
    993       }
    994       ctx.stroke();
    995     } else if (type === 'rect' && px.length >= 2 && px[1].x != null) {
    996       const x0 = Math.min(px[0].x, px[1].x);
    997       const y0 = Math.min(px[0].y, px[1].y);
    998       const w  = Math.abs(px[1].x - px[0].x);
    999       const h  = Math.abs(px[1].y - px[0].y);
   1000       ctx.strokeStyle = '#f0c040';
   1001       ctx.fillStyle   = 'rgba(240,192,64,0.07)';
   1002       ctx.lineWidth   = 1.5;
   1003       ctx.strokeRect(x0, y0, w, h);
   1004       ctx.fillRect(x0, y0, w, h);
   1005     } else if (type === 'text' && text) {
   1006       ctx.fillStyle = '#e0d0ff';
   1007       ctx.font = '13px "Segoe UI", system-ui, sans-serif';
   1008       ctx.fillText(text, px[0].x, px[0].y);
   1009     }
   1010 
   1011     ctx.globalAlpha = 1;
   1012   }
   1013 
   1014   requestAnimationFrame(gymDrawFrame);
   1015 
   1016   // ── Mode switching ────────────────────────────────────────────────────────
   1017 
   1018   const GYM_RIGHT_OFFSET = 55;
   1019 
   1020   function setGymTool(tool) {
   1021     gymActiveTool = tool;
   1022     document.querySelectorAll('.gym-tool-btn').forEach(b => b.classList.remove('active'));
   1023     if (tool) {
   1024       document.getElementById('tool-' + tool).classList.add('active');
   1025       gymCanvas.style.pointerEvents = 'auto';
   1026       gymCanvas.style.cursor = tool === 'text' ? 'text' : 'crosshair';
   1027     } else {
   1028       gymCanvas.style.pointerEvents = 'none';
   1029       gymCanvas.style.cursor = 'default';
   1030     }
   1031     const hints = {
   1032       line:    'Click and drag to draw a straight line.',
   1033       pencil:  'Click and drag to draw freehand.',
   1034       text:    'Click to place a text label.',
   1035       rect:    'Click and drag to draw a rectangle.',
   1036     };
   1037     document.getElementById('tool-hint').textContent =
   1038       tool ? hints[tool] : 'Select a tool or pan/zoom freely.';
   1039   }
   1040 
   1041   // Tool buttons — click active tool to deselect
   1042   ['line', 'pencil', 'text', 'rect'].forEach(tool => {
   1043     document.getElementById('tool-' + tool).addEventListener('click', () => {
   1044       setGymTool(gymActiveTool === tool ? null : tool);
   1045     });
   1046   });
   1047 
   1048   document.getElementById('clear-prediction-btn').addEventListener('click', () => {
   1049     if (!gymActivePrediction || !sessionId) return;
   1050     fetch('/api/predict/cancel', {
   1051       method: 'POST',
   1052       headers: { 'Content-Type': 'application/json' },
   1053       body: JSON.stringify({ session_id: sessionId }),
   1054     });
   1055     gymActivePrediction = null;
   1056     updatePredictionInfo();
   1057   });
   1058 
   1059   async function gymFastForwardToClose() {
   1060     if (!gymActivePrediction || gymFastForwarding) return;
   1061     stopAutoPlay();
   1062     gymFastForwarding = true;
   1063     document.getElementById('ff-to-position-btn').disabled = true;
   1064     document.getElementById('next-btn').disabled = true;
   1065     document.getElementById('auto-play-btn').disabled = true;
   1066 
   1067     while (gymActivePrediction && gymLastLogical < gymActivePrediction.logTarget) {
   1068       const res = await fetch('/api/next', {
   1069         method: 'POST',
   1070         headers: { 'Content-Type': 'application/json' },
   1071         body: JSON.stringify({ session_id: sessionId }),
   1072       });
   1073 
   1074       if (res.status === 409) {
   1075         document.getElementById('next-btn').textContent = 'End of data';
   1076         document.getElementById('next-btn').disabled = true;
   1077         break;
   1078       }
   1079 
   1080       const data = await res.json();
   1081       const c = data.candle;
   1082       candleSeries.update({ time: c.ts, open: c.open, high: c.high, low: c.low, close: c.close });
   1083       chart.timeScale().scrollToRealTime();
   1084       gymLastLogical++;
   1085       currentPrice = c.close;
   1086 
   1087       if (data.resolved) {
   1088         handleResolvedPrediction(data.resolved);
   1089         break;
   1090       } else {
   1091         updatePredictionInfo();
   1092       }
   1093 
   1094       await new Promise(r => setTimeout(r, 120));
   1095     }
   1096 
   1097     gymFastForwarding = false;
   1098     document.getElementById('next-btn').disabled = false;
   1099     document.getElementById('auto-play-btn').disabled = false;
   1100     updatePredictionInfo();
   1101   }
   1102 
   1103   document.getElementById('ff-to-position-btn').addEventListener('click', gymFastForwardToClose);
   1104 
   1105   document.getElementById('clear-drawings-btn').addEventListener('click', () => {
   1106     gymDrawings.length = 0;
   1107   });
   1108 
   1109   // ── Keyboard shortcuts ────────────────────────────────────────────────────
   1110 
   1111   document.addEventListener('keydown', e => {
   1112     if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
   1113     if (e.key === 'Escape') {
   1114       if (gymMode && gymActiveTool !== null) setGymTool(null);
   1115     } else if (e.key === ' ') {
   1116       e.preventDefault();
   1117       const btn = document.getElementById('next-btn');
   1118       if (!btn.disabled) btn.click();
   1119     } else if (e.key === 'c' || e.key === 'C') {
   1120       const btn = document.getElementById('ff-to-position-btn');
   1121       if (!btn.disabled) btn.click();
   1122     }
   1123   });
   1124 
   1125   // ── Double-click prediction (no tool selected) ────────────────────────────
   1126 
   1127   document.addEventListener('dblclick', e => {
   1128     if (!gymMode || gymActiveTool !== null) return;
   1129     if (!document.getElementById('chart-container').contains(e.target)) return;
   1130     e.stopPropagation();
   1131     e.preventDefault();
   1132     const r = gymCanvas.getBoundingClientRect();
   1133     const x = e.clientX - r.left;
   1134     const y = e.clientY - r.top;
   1135     const { logical, price } = pxToChart(x, y);
   1136     if (logical == null || price == null) return;
   1137     const barsAhead = Math.round(logical) - gymLastLogical;
   1138     if (barsAhead <= 0) return;
   1139     const direction = price > currentPrice ? 'long' : 'short';
   1140     const feePct = parseFloat(document.getElementById('gym-fee-input').value) || 0;
   1141     fetch('/api/predict', {
   1142       method: 'POST',
   1143       headers: { 'Content-Type': 'application/json' },
   1144       body: JSON.stringify({ session_id: sessionId, bars_ahead: barsAhead, direction, fee_pct: feePct }),
   1145     }).then(r => r.json()).then(data => {
   1146       gymActivePrediction = {
   1147         logTarget:  gymLastLogical + barsAhead,
   1148         entryPrice: data.entry_price,
   1149         direction,
   1150       };
   1151       updatePredictionInfo();
   1152     });
   1153   }, true);
   1154 
   1155   // ── Boot ──────────────────────────────────────────────────────────────────
   1156 
   1157   initChart();
   1158   chart.timeScale().applyOptions({ rightOffset: GYM_RIGHT_OFFSET });
   1159   resizeGymCanvas();
   1160   setGymTool(null);
   1161   startSession();
   1162 </script>
   1163 </body>
   1164 </html>