Guest

ALTERNATIVA 2

Apr 3rd, 2026
14
0
Never
Not a member of GistPad yet? Sign Up, it unlocks many cool features!
None 176.85 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="it">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Vertical Spread Tracker</title>
  7. <style>
  8. :root {
  9. --bg: #0f1117; --surface: #1a1d2e; --surface2: #22263a;
  10. --border: #2e3250; --accent: #6c63ff; --green: #00d395;
  11. --red: #ff4d6d; --yellow: #f9c74f; --text: #e2e8f0;
  12. --muted: #8892b0; --white: #ffffff;
  13. }
  14. * { margin:0; padding:0; box-sizing:border-box; }
  15. body { background:var(--bg); color:var(--text); font-family:'Segoe UI',sans-serif; font-size:14px; min-height:100vh; }
  16.  
  17. /* HEADER */
  18. header { background:var(--surface); border-bottom:1px solid var(--border); padding:14px 24px; display:flex; align-items:center; justify-content:space-between; }
  19. header h1 { font-size:18px; font-weight:700; color:var(--white); display:flex; align-items:center; gap:10px; }
  20. header h1 span { background:var(--accent); border-radius:8px; padding:4px 10px; font-size:13px; }
  21. .header-actions { display:flex; gap:10px; }
  22.  
  23. /* BUTTONS */
  24. .btn { padding:8px 16px; border-radius:8px; border:none; cursor:pointer; font-size:13px; font-weight:600; transition:all .2s; }
  25. .btn-primary { background:var(--accent); color:#fff; }
  26. .btn-primary:hover { background:#7c75ff; }
  27. .btn-success { background:var(--green); color:#000; }
  28. .btn-success:hover { opacity:.85; }
  29. .btn-danger { background:var(--red); color:#fff; }
  30. .btn-outline { background:transparent; border:1px solid var(--border); color:var(--text); }
  31. .btn-outline:hover { border-color:var(--accent); color:var(--accent); }
  32. .btn-sm { padding:5px 10px; font-size:12px; }
  33.  
  34. /* LAYOUT */
  35. .container { max-width:1400px; margin:0 auto; padding:20px; }
  36. .grid-4 { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:24px; }
  37. .grid-2 { display:grid; grid-template-columns:repeat(2,1fr); gap:16px; margin-bottom:24px; }
  38.  
  39. /* KPI CARDS */
  40. .kpi { background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:18px; }
  41. .kpi.highlight { border-color:var(--accent); }
  42. .kpi label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px; display:block; margin-bottom:8px; }
  43. .kpi .val { font-size:26px; font-weight:700; }
  44. .kpi .sub { font-size:11px; color:var(--muted); margin-top:4px; }
  45. .green { color:var(--green); } .red { color:var(--red); } .yellow { color:var(--yellow); }
  46.  
  47. /* SECTION */
  48. .section { background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:20px; margin-bottom:24px; }
  49. .section-title { font-size:15px; font-weight:700; color:var(--white); margin-bottom:16px; display:flex; align-items:center; justify-content:space-between; }
  50.  
  51. /* FORM */
  52. .form-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:12px; }
  53. .form-group { display:flex; flex-direction:column; gap:5px; }
  54. .form-group label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px; }
  55. .form-group input, .form-group select { background:var(--surface2); border:1px solid var(--border); border-radius:8px; padding:8px 12px; color:var(--text); font-size:13px; outline:none; transition:.2s; }
  56. .form-group input:focus, .form-group select:focus { border-color:var(--accent); }
  57. .form-group select option { background:var(--surface2); }
  58. .form-row { display:flex; gap:12px; align-items:flex-end; flex-wrap:wrap; margin-top:12px; }
  59.  
  60. /* CALC PREVIEW */
  61. .calc-preview { background:var(--surface2); border:1px solid var(--border); border-radius:10px; padding:14px; display:grid; grid-template-columns:repeat(5,1fr); gap:10px; margin-top:14px; }
  62. .calc-item label { font-size:10px; color:var(--muted); display:block; margin-bottom:4px; }
  63. .calc-item span { font-size:15px; font-weight:700; }
  64.  
  65. /* TABLE */
  66. .table-wrap { overflow-x:auto; }
  67. table { width:100%; border-collapse:collapse; font-size:13px; }
  68. thead tr { background:var(--surface2); }
  69. th { padding:10px 12px; text-align:left; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.5px; white-space:nowrap; border-bottom:1px solid var(--border); }
  70. td { padding:10px 12px; border-bottom:1px solid var(--border); white-space:nowrap; vertical-align:middle; }
  71. tr:hover td { background:rgba(108,99,255,.05); }
  72. .badge { display:inline-block; padding:3px 8px; border-radius:20px; font-size:11px; font-weight:600; }
  73. .badge-call { background:rgba(108,99,255,.2); color:#a29bff; }
  74. .badge-put { background:rgba(249,199,79,.15); color:var(--yellow); }
  75. .badge-open { background:rgba(0,211,149,.15); color:var(--green); }
  76. .badge-closed { background:rgba(142,142,142,.15); color:#999; }
  77. .badge-stop { background:rgba(255,77,109,.15); color:var(--red); }
  78. .badge-win { background:rgba(0,211,149,.2); color:var(--green); }
  79. .badge-loss { background:rgba(255,77,109,.2); color:var(--red); }
  80.  
  81. /* MODAL */
  82. .modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,.7); z-index:100; align-items:center; justify-content:center; }
  83. .modal-overlay.active { display:flex; }
  84. .modal { background:var(--surface); border:1px solid var(--border); border-radius:16px; padding:28px; width:90%; max-width:600px; }
  85. .modal h2 { font-size:17px; margin-bottom:20px; color:var(--white); }
  86.  
  87. /* TABS */
  88. .tabs { display:flex; gap:4px; background:var(--surface2); border-radius:10px; padding:4px; margin-bottom:20px; width:fit-content; }
  89. .tab { padding:7px 18px; border-radius:8px; cursor:pointer; font-size:13px; font-weight:600; color:var(--muted); transition:.2s; border:none; background:transparent; }
  90. .tab.active { background:var(--accent); color:#fff; }
  91.  
  92. /* ALERT */
  93. .alert { padding:10px 14px; border-radius:8px; font-size:13px; margin-bottom:12px; display:none; }
  94. .alert.success { background:rgba(0,211,149,.12); border:1px solid var(--green); color:var(--green); display:block; }
  95. .alert.error { background:rgba(255,77,109,.12); border:1px solid var(--red); color:var(--red); display:block; }
  96.  
  97. /* TOAST NOTIFICATION */
  98. .toast { position:fixed; bottom:28px; right:28px; z-index:999; padding:14px 22px; border-radius:12px; font-size:14px; font-weight:600; display:flex; align-items:center; gap:10px; box-shadow:0 8px 32px rgba(0,0,0,.4); transform:translateY(80px); opacity:0; transition:all .35s cubic-bezier(.21,1.02,.73,1); pointer-events:none; }
  99. .toast.show { transform:translateY(0); opacity:1; }
  100. .toast.success { background:#00d395; color:#000; }
  101. .toast.error { background:#ff4d6d; color:#fff; }
  102. .toast.info { background:#4cc9f0; color:#000; }
  103.  
  104. /* AUTO-SAVE INDICATOR */
  105. .autosave-dot { width:8px; height:8px; border-radius:50%; background:var(--green); display:inline-block; margin-left:8px; animation:pulse 2s infinite; }
  106. @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
  107.  
  108. /* INPUT ERROR STATE */
  109. .input-error { border-color:var(--red) !important; background:rgba(255,77,109,.08) !important; animation:shake .3s ease; }
  110. @keyframes shake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-4px)} 75%{transform:translateX(4px)} }
  111.  
  112. /* TABLE RESPONSIVE β€” no horizontal scroll */
  113. .table-wrap { overflow-x:visible; }
  114. table { width:100%; border-collapse:collapse; font-size:12px; table-layout:fixed; }
  115. th, td { padding:8px 6px; overflow:hidden; text-overflow:ellipsis; }
  116. th { white-space:normal; word-break:break-word; }
  117. td { white-space:normal; word-break:break-word; }
  118.  
  119. /* NAV */
  120. nav { background:var(--surface); border-bottom:1px solid var(--border); padding:0 24px; display:flex; gap:4px; }
  121. .nav-btn { padding:12px 20px; background:none; border:none; border-bottom:3px solid transparent; color:var(--muted); font-size:13px; font-weight:600; cursor:pointer; transition:.2s; margin-bottom:-1px; }
  122. .nav-btn:hover { color:var(--text); }
  123. .nav-btn.active { color:var(--accent); border-bottom-color:var(--accent); }
  124.  
  125. /* PAGES */
  126. .page { display:none; }
  127. .page.active { display:block; }
  128.  
  129. /* ANALYTICS */
  130. .grid-3 { display:grid; grid-template-columns:repeat(3,1fr); gap:16px; margin-bottom:24px; }
  131. .grid-2-1 { display:grid; grid-template-columns:2fr 1fr; gap:16px; margin-bottom:24px; }
  132. .grid-1-1 { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:24px; }
  133. .chart-box { background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:20px; }
  134. .chart-box .chart-title { font-size:13px; font-weight:700; color:var(--white); margin-bottom:4px; }
  135. .chart-box .chart-sub { font-size:11px; color:var(--muted); margin-bottom:16px; }
  136. .chart-box canvas { max-height:260px; }
  137. .chart-box.tall canvas { max-height:320px; }
  138.  
  139. /* STAT TABLE */
  140. .stat-row { display:flex; justify-content:space-between; align-items:center; padding:9px 0; border-bottom:1px solid var(--border); font-size:13px; }
  141. .stat-row:last-child { border-bottom:none; }
  142. .stat-label { color:var(--muted); }
  143. .stat-val { font-weight:700; }
  144.  
  145. /* EQUITY TICKER */
  146. .equity-change { font-size:12px; padding:2px 8px; border-radius:20px; margin-left:8px; }
  147. .equity-up { background:rgba(0,211,149,.15); color:var(--green); }
  148. .equity-down { background:rgba(255,77,109,.15); color:var(--red); }
  149.  
  150. /* HEATMAP */
  151. .heatmap { display:grid; grid-template-columns:repeat(7,1fr); gap:4px; }
  152. .heatmap-cell { aspect-ratio:1; border-radius:4px; display:flex; align-items:center; justify-content:center; font-size:9px; font-weight:700; cursor:default; transition:.2s; }
  153. .heatmap-cell:hover { transform:scale(1.1); }
  154.  
  155. /* NO DATA */
  156. .no-data-chart { display:flex; flex-direction:column; align-items:center; justify-content:center; height:160px; color:var(--muted); font-size:13px; gap:8px; }
  157. .no-data-chart span { font-size:32px; }
  158.  
  159. /* EQUITY FILTER BUTTONS */
  160. .eq-btn { padding:5px 12px; border-radius:20px; border:1px solid var(--border); background:transparent; color:var(--muted); font-size:12px; font-weight:600; cursor:pointer; transition:.2s; }
  161. .eq-btn:hover { border-color:var(--accent); color:var(--accent); }
  162. .eq-btn.active { background:var(--accent); border-color:var(--accent); color:#fff; }
  163.  
  164. /* EXPIRY CALENDAR */
  165. .cal-filter-bar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
  166. .cal-yr-btn { padding:5px 14px; border-radius:20px; border:1px solid var(--border); background:transparent; color:var(--muted); font-size:12px; font-weight:600; cursor:pointer; transition:all .2s; }
  167. .cal-yr-btn.active, .cal-yr-btn:hover { background:var(--accent); color:#fff; border-color:var(--accent); }
  168. .cal-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); gap:16px; }
  169. .cal-month { background:var(--surface2); border:1px solid var(--border); border-radius:12px; overflow:hidden; transition:border-color .2s; }
  170. .cal-month:hover { border-color:var(--accent); }
  171. .cal-month-header { padding:12px 16px 10px; font-size:14px; font-weight:700; color:var(--white); display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--border); }
  172. .cal-month-header .cal-count { font-size:11px; padding:2px 8px; border-radius:20px; background:rgba(108,99,255,.2); color:var(--accent); }
  173. .cal-month-stats { display:grid; grid-template-columns:1fr 1fr 1fr; gap:0; border-bottom:1px solid var(--border); }
  174. .cal-ms { padding:8px 10px; text-align:center; border-right:1px solid var(--border); }
  175. .cal-ms:last-child { border-right:none; }
  176. .cal-ms-val { font-size:16px; font-weight:800; color:var(--white); }
  177. .cal-ms-lbl { font-size:9px; text-transform:uppercase; letter-spacing:.5px; color:var(--muted); margin-top:2px; }
  178. .cal-type-bar { display:flex; height:6px; border-radius:3px; overflow:hidden; margin:0 14px 8px; }
  179. .cal-type-seg { transition:width .3s; }
  180. .cal-outcome-row { display:flex; gap:8px; padding:4px 14px 8px; flex-wrap:wrap; }
  181. .cal-outcome-pill { font-size:10px; padding:2px 8px; border-radius:12px; font-weight:600; }
  182. .cal-trade-row { padding:7px 14px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid rgba(255,255,255,.04); font-size:12px; }
  183. .cal-trade-row:last-child { border-bottom:none; }
  184. .cal-trade-left { display:flex; flex-direction:column; gap:2px; }
  185. .cal-trade-ticker { font-weight:700; color:var(--white); }
  186. .cal-trade-info { color:var(--muted); font-size:11px; }
  187. .cal-trade-right { display:flex; flex-direction:column; align-items:flex-end; gap:2px; }
  188. .cal-past { opacity:.5; }
  189. .cal-soon { border-left:3px solid var(--yellow); }
  190. .cal-pnl-bar { display:flex; align-items:center; gap:6px; padding:6px 14px 10px; }
  191. .cal-pnl-track { flex:1; height:8px; background:rgba(255,255,255,.06); border-radius:4px; overflow:hidden; position:relative; }
  192. .cal-pnl-fill { height:100%; border-radius:4px; transition:width .3s; }
  193. .cal-pnl-val { font-size:11px; font-weight:700; min-width:60px; text-align:right; }
  194. /* Stats enhanced */
  195. .stat-section-title { font-size:11px; text-transform:uppercase; letter-spacing:1px; color:var(--accent); font-weight:700; padding:10px 0 6px; border-bottom:1px solid var(--border); margin-bottom:4px; }
  196. .cal-today { border-left:3px solid var(--red); }
  197.  
  198. /* TRADE CARDS */
  199. .trade-card { background:var(--surface2); border:1px solid var(--border); border-radius:12px; padding:16px 20px; margin-bottom:10px; display:grid; grid-template-columns:auto 1fr auto; gap:16px; align-items:center; transition:.2s; }
  200. .trade-card:hover { border-color:var(--accent); box-shadow:0 0 0 1px rgba(108,99,255,.2); }
  201. .trade-card.status-open { border-left:3px solid var(--green); }
  202. .trade-card.status-closed { border-left:3px solid #555; }
  203. .trade-card.status-stop { border-left:3px solid var(--red); }
  204. .trade-card.status-early { border-left:3px solid var(--yellow); }
  205. .tc-left { display:flex; flex-direction:column; align-items:center; gap:4px; min-width:64px; }
  206. .tc-ticker { font-size:20px; font-weight:800; color:var(--white); letter-spacing:.5px; }
  207. .tc-asset { font-size:10px; color:var(--muted); text-transform:uppercase; }
  208. .tc-mid { display:grid; grid-template-columns:repeat(5,1fr); gap:8px 16px; }
  209. .tc-field { display:flex; flex-direction:column; gap:2px; }
  210. .tc-field span { font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.3px; }
  211. .tc-field strong { font-size:13px; color:var(--text); font-weight:600; }
  212. .tc-right { display:flex; flex-direction:column; align-items:flex-end; gap:8px; min-width:120px; }
  213. .tc-pnl { font-size:22px; font-weight:800; }
  214. .tc-roi { font-size:12px; font-weight:600; }
  215. .tc-actions { display:flex; gap:6px; flex-wrap:wrap; justify-content:flex-end; }
  216. .tc-divider { height:1px; background:var(--border); margin:0 0 4px 0; }
  217. .tc-dates { display:flex; gap:16px; }
  218. .tc-dates .tc-field strong { font-size:12px; }
  219.  
  220. @media(max-width:1100px){ .grid-2-1{grid-template-columns:1fr;} .grid-3{grid-template-columns:repeat(2,1fr);} }
  221. @media(max-width:900px){ .grid-4{grid-template-columns:repeat(2,1fr);} .calc-preview{grid-template-columns:repeat(2,1fr);} .grid-1-1{grid-template-columns:1fr;} }
  222. @media(max-width:600px){ .grid-4{grid-template-columns:1fr;} .grid-3{grid-template-columns:1fr;} }
  223. </style>
  224. </head>
  225. <body>
  226.  
  227. <header>
  228. <h1>πŸ“Š Vertical Spread Tracker <span>Options</span><span class="autosave-dot" title="Auto-salvataggio attivo nel browser"></span></h1>
  229. <div class="header-actions">
  230. <button class="btn btn-outline" onclick="exportCSV()" title="Esporta tutti i trade in formato CSV per Excel">⬇ CSV</button>
  231. <button class="btn btn-outline" style="border-color:#4cc9f0;color:#4cc9f0;" onclick="importJSON()" title="Ripristina un backup JSON salvato in precedenza">⬆ Ripristina JSON</button>
  232. <button class="btn btn-outline" style="border-color:#f9c74f;color:#f9c74f;" onclick="exportJSON()" title="Salva tutti i tuoi trade in un file JSON sul PC">πŸ’Ύ Salva JSON</button>
  233. <button class="btn btn-primary" onclick="openModal()">οΌ‹ Nuovo Trade</button>
  234. </div>
  235. </header>
  236.  
  237. <!-- INPUT NASCOSTO PER IMPORT FILE -->
  238. <input type="file" id="jsonFileInput" accept=".json" style="display:none" onchange="loadJSONFile(event)" />
  239.  
  240. <!-- TOAST -->
  241. <div class="toast" id="toast"></div>
  242.  
  243. <nav>
  244. <button class="nav-btn active" onclick="showPage('dashboard',this)">πŸ“‹ Dashboard</button>
  245. <button class="nav-btn" onclick="showPage('analytics',this)">πŸ“ˆ Analytics</button>
  246. </nav>
  247.  
  248. <!-- ═══════════════════════════ PAGE: DASHBOARD ═══════════════════════════ -->
  249. <div class="page active" id="page-dashboard">
  250. <div class="container">
  251.  
  252. <!-- KPI ROW 1: 4 box principali -->
  253. <div class="grid-4">
  254. <div class="kpi"><label>Trades Aperti</label><div class="val yellow" id="k-open">0</div><div class="sub">posizioni attive</div></div>
  255. <div class="kpi"><label>Margine Impiegato</label><div class="val yellow" id="k-margin">$0,00</div><div class="sub">somma margine broker posizioni aperte</div></div>
  256. <div class="kpi highlight"><label>P&L Realizzato</label><div class="val green" id="k-pnl">$0,00</div><div class="sub">al netto di tutte le fee</div></div>
  257. <div class="kpi"><label>Win Rate</label><div class="val green" id="k-wr">0%</div><div class="sub" id="k-wr-sub">0 win / 0 chiusi</div></div>
  258. </div>
  259.  
  260. <!-- KPI ROW 2: Dettaglio WR + ROI medio + AVERAGE box -->
  261. <div style="display:grid;grid-template-columns:1fr 1fr 2fr;gap:16px;margin-bottom:24px;">
  262.  
  263. <!-- Dettaglio Win Rate -->
  264. <div class="kpi">
  265. <label>Dettaglio Win Rate</label>
  266. <div style="display:flex;flex-direction:column;gap:6px;margin-top:4px;" id="k-wr-detail">
  267. <div style="display:flex;justify-content:space-between;font-size:13px;"><span style="color:var(--muted)">βœ… Scadenza</span><span id="k-wr-exp" class="green">0 (0%)</span></div>
  268. <div style="display:flex;justify-content:space-between;font-size:13px;"><span style="color:var(--muted)">↩ Uscita Ant.</span><span id="k-wr-early" class="yellow">0 (0%)</span></div>
  269. <div style="display:flex;justify-content:space-between;font-size:13px;"><span style="color:var(--muted)">πŸ›‘ Stop Loss</span><span id="k-wr-stop" class="red">0 (0%)</span></div>
  270. </div>
  271. </div>
  272.  
  273. <!-- ROI Medio per Trade -->
  274. <div class="kpi">
  275. <label>ROI Medio Atteso per Trade</label>
  276. <div class="val green" id="k-roi-avg">0,00%</div>
  277. <div class="sub">media ROI atteso (aperti + chiusi)</div>
  278. </div>
  279.  
  280. <!-- AVERAGE box: 4 metriche in griglia -->
  281. <div class="kpi">
  282. <label>Average β€” medie su tutti i trade</label>
  283. <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:10px;margin-top:8px;">
  284. <div>
  285. <div style="font-size:10px;color:var(--muted);text-transform:uppercase;margin-bottom:4px;">Max Profitto Netto</div>
  286. <div style="font-size:15px;font-weight:700;color:var(--green);" id="k-avg-profit">β€”</div>
  287. </div>
  288. <div>
  289. <div style="font-size:10px;color:var(--muted);text-transform:uppercase;margin-bottom:4px;">Max Perdita (SL)</div>
  290. <div style="font-size:15px;font-weight:700;color:var(--red);" id="k-avg-loss">β€”</div>
  291. </div>
  292. <div>
  293. <div style="font-size:10px;color:var(--muted);text-transform:uppercase;margin-bottom:4px;">Margine Broker</div>
  294. <div style="font-size:15px;font-weight:700;color:var(--yellow);" id="k-avg-margin">β€”</div>
  295. </div>
  296. <div>
  297. <div style="font-size:10px;color:var(--muted);text-transform:uppercase;margin-bottom:4px;">R/R Ratio medio</div>
  298. <div style="font-size:15px;font-weight:700;" id="k-avg-rr">β€”</div>
  299. </div>
  300. </div>
  301. </div>
  302.  
  303. </div>
  304.  
  305. <!-- TRADE ENTRY SECTION -->
  306. <div class="section">
  307. <div class="section-title">Aggiungi / Modifica Trade</div>
  308. <div id="formAlert" class="alert"></div>
  309.  
  310. <!-- TABS -->
  311. <div class="tabs">
  312. <button class="tab active" onclick="setSpreadType('bull_put',this)">Bull Put Spread</button>
  313. <button class="tab" onclick="setSpreadType('bear_call',this)">Bear Call Spread</button>
  314. </div>
  315.  
  316. <input type="hidden" id="editId" value="">
  317. <input type="hidden" id="spreadType" value="bull_put">
  318.  
  319. <div class="form-grid">
  320. <div class="form-group">
  321. <label>Tipo Strumento</label>
  322. <select id="f-asset-type" onchange="onAssetTypeChange()">
  323. <option value="stock">Stock</option>
  324. <option value="etf">ETF</option>
  325. <option value="future_index">Future Index</option>
  326. <option value="future_mp">Future M.P.</option>
  327. <option value="future_fx">Future Valute</option>
  328. </select>
  329. </div>
  330. <div class="form-group"><label>Ticker</label><input type="text" id="f-ticker" placeholder="es. AAPL" oninput="this.value=this.value.toUpperCase()" /></div>
  331. <div class="form-group"><label>Data Apertura</label><input type="date" id="f-open-date" /></div>
  332. <div class="form-group"><label>Scadenza</label><input type="date" id="f-expiry" /></div>
  333. <div class="form-group"><label>Contratti</label><input type="number" id="f-contracts" value="1" min="1" onchange="calcPreview()" /></div>
  334. <div class="form-group">
  335. <label>Moltiplicatore</label>
  336. <input type="text" inputmode="numeric" id="f-multiplier" value="100" oninput="calcPreview()" />
  337. </div>
  338. <div class="form-group">
  339. <label>Margine Broker ($)</label>
  340. <input type="text" inputmode="decimal" id="f-margin-broker" placeholder="es. 760" oninput="calcPreview()" />
  341. </div>
  342. <div class="form-group"><label id="sell-strike-lbl">Put VENDUTA (OTM)</label><input type="text" inputmode="decimal" id="f-sell-strike" placeholder="es. 30" oninput="validateStrikesLive(); calcPreview();" /></div>
  343. <div class="form-group"><label id="buy-strike-lbl">Put COMPRATA (piΓΉ bassa)</label><input type="text" inputmode="decimal" id="f-buy-strike" placeholder="es. 25" oninput="validateStrikesLive(); calcPreview();" /></div>
  344. <div class="form-group"><label>Premio Netto Incassato ($)</label><input type="text" inputmode="decimal" id="f-premium" placeholder="es. 0,26" oninput="calcPreview()" /></div>
  345. <div class="form-group"><label>Commissioni ($)</label><input type="text" inputmode="decimal" id="f-fees" placeholder="es. 6,50" value="0" oninput="calcPreview()" /></div>
  346. </div>
  347.  
  348. <!-- STRIKE ERROR -->
  349. <div id="strikeError" style="display:none;background:rgba(255,77,109,.1);border:1px solid var(--red);border-radius:8px;padding:10px 14px;font-size:13px;color:var(--red);margin-top:10px;">
  350. ⚠ <strong>Strike non valido:</strong> per un Bull Put Spread la Put COMPRATA deve essere <em>inferiore</em> alla Put VENDUTA. Per un Bear Call Spread la Call COMPRATA deve essere <em>superiore</em> alla Call VENDUTA.
  351. </div>
  352.  
  353. <!-- CALC PREVIEW -->
  354. <div class="calc-preview" id="calcPreview" style="grid-template-columns:repeat(4,1fr);">
  355. <div class="calc-item">
  356. <label>Max Profitto Netto</label>
  357. <span class="green" id="cp-maxprofit">β€”</span>
  358. <small style="color:var(--muted);font-size:10px;display:block;margin-top:3px;">Premio Γ— Molt. Γ— Cont. βˆ’ Comm.</small>
  359. </div>
  360. <div class="calc-item">
  361. <label>Max Perdita (con SL)</label>
  362. <span class="red" id="cp-maxloss">β€”</span>
  363. <small style="color:var(--muted);font-size:10px;display:block;margin-top:3px;">SL debit + Comm. βˆ’ Max Profitto</small>
  364. </div>
  365. <div class="calc-item">
  366. <label>R/R Ratio</label>
  367. <span id="cp-rr">β€”</span>
  368. <small style="color:var(--muted);font-size:10px;display:block;margin-top:3px;">Max Perdita SL Γ· Max Profitto</small>
  369. </div>
  370. <div class="calc-item">
  371. <label>ROI % (su margine broker)</label>
  372. <span class="green" id="cp-roi">β€”</span>
  373. <small style="color:var(--muted);font-size:10px;display:block;margin-top:3px;">Max Profitto Netto / Margine</small>
  374. </div>
  375. </div>
  376.  
  377. <div class="form-row">
  378. <button class="btn btn-success" onclick="saveTrade()">βœ“ Salva Trade</button>
  379. <button class="btn btn-outline" onclick="resetForm()">βœ• Reset</button>
  380. </div>
  381. </div>
  382.  
  383. <!-- TRADES TABLE -->
  384. <div class="section">
  385. <div class="section-title">
  386. <span>Trades Recenti</span>
  387. <div style="display:flex;gap:8px;">
  388. <select id="filterStatus" onchange="renderTable()" style="background:var(--surface2);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:6px 10px;font-size:12px;">
  389. <option value="all">Tutti i stati</option>
  390. <option value="open">Aperti</option>
  391. <option value="closed">Chiusi (Win)</option>
  392. <option value="stop">Stop Loss</option>
  393. </select>
  394. <select id="filterType" onchange="renderTable()" style="background:var(--surface2);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:6px 10px;font-size:12px;">
  395. <option value="all">Tutti i tipi</option>
  396. <option value="bull_put">Bull Put</option>
  397. <option value="bear_call">Bear Call</option>
  398. </select>
  399. </div>
  400. </div>
  401. <div id="tradeList"></div>
  402. <div id="noTrades" style="text-align:center;padding:40px;color:var(--muted);">Nessun trade ancora. Aggiungi il tuo primo trade!</div>
  403. </div>
  404.  
  405. </div>
  406. </div><!-- end page-dashboard -->
  407.  
  408. <!-- ═══════════════════════════ PAGE: ANALYTICS ═══════════════════════════ -->
  409. <div class="page" id="page-analytics">
  410. <div class="container">
  411.  
  412. <!-- KPI BAR -->
  413. <div class="grid-4" style="margin-bottom:20px;">
  414. <div class="kpi highlight">
  415. <label>P&L Cumulativo</label>
  416. <div class="val green" id="an-equity">$0,00</div>
  417. <div class="sub" id="an-equity-sub">su 0 trade chiusi</div>
  418. </div>
  419. <div class="kpi">
  420. <label>Profit Factor</label>
  421. <div class="val" id="an-pf">β€”</div>
  422. <div class="sub">Gross Win Γ· Gross Loss (>1 = profittevole)</div>
  423. </div>
  424. <div class="kpi">
  425. <label>Max Drawdown</label>
  426. <div class="val red" id="an-dd">$0,00</div>
  427. <div class="sub">peggior perdita cumulata consecutiva</div>
  428. </div>
  429. <div class="kpi">
  430. <label>Avg Win / Avg Loss</label>
  431. <div class="val" id="an-avgwl">β€”</div>
  432. <div class="sub">qualitΓ  media per trade</div>
  433. </div>
  434. </div>
  435.  
  436. <!-- EQUITY CURVE (full width) con filtro temporale -->
  437. <div class="chart-box" style="margin-bottom:20px;">
  438. <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:4px;">
  439. <div>
  440. <div class="chart-title">Equity Curve β€” P&L Cumulativo nel Tempo</div>
  441. <div class="chart-sub">Ogni punto = chiusura di un trade. Filtra per periodo per vedere l'andamento.</div>
  442. </div>
  443. <div style="display:flex;gap:6px;flex-wrap:wrap;">
  444. <button class="eq-btn active" onclick="setEqFilter('all',this)">Tutto</button>
  445. <button class="eq-btn" onclick="setEqFilter('1y',this)">1 Anno</button>
  446. <button class="eq-btn" onclick="setEqFilter('6m',this)">6 Mesi</button>
  447. <button class="eq-btn" onclick="setEqFilter('3m',this)">3 Mesi</button>
  448. <button class="eq-btn" onclick="setEqFilter('1m',this)">1 Mese</button>
  449. </div>
  450. </div>
  451. <canvas id="chartEquity" style="max-height:300px;"></canvas>
  452. <div class="no-data-chart" id="nd-equity"><span>πŸ“‰</span>Nessun trade chiuso nel periodo selezionato</div>
  453. </div>
  454.  
  455. <!-- ROW 2: Mensile (waterfall) + Drawdown -->
  456. <div class="grid-1-1" style="margin-bottom:20px;">
  457. <div class="chart-box">
  458. <div class="chart-title">P&L per Mese</div>
  459. <div class="chart-sub">Profitto/perdita realizzata aggregata per mese di chiusura</div>
  460. <canvas id="chartMonthly" style="max-height:240px;"></canvas>
  461. <div class="no-data-chart" id="nd-monthly"><span>πŸ“…</span>Nessun trade chiuso</div>
  462. </div>
  463. <div class="chart-box">
  464. <div class="chart-title">Drawdown nel Tempo</div>
  465. <div class="chart-sub">Distanza dal picco massimo di equity β€” misura la resilienza del metodo</div>
  466. <canvas id="chartDrawdown" style="max-height:240px;"></canvas>
  467. <div class="no-data-chart" id="nd-drawdown"><span>πŸ“‰</span>Nessun trade chiuso</div>
  468. </div>
  469. </div>
  470.  
  471. <!-- ROW 3: Outcome donut + Holding Period + Asset Type -->
  472. <div class="grid-3" style="margin-bottom:20px;">
  473. <div class="chart-box">
  474. <div class="chart-title">Distribuzione Esiti</div>
  475. <div class="chart-sub">Scadenza Β· Stop Loss Β· Uscita Anticipata</div>
  476. <canvas id="chartDonut" style="max-height:200px;"></canvas>
  477. <div class="no-data-chart" id="nd-donut"><span>🍩</span>Nessun trade chiuso</div>
  478. </div>
  479. <div class="chart-box">
  480. <div class="chart-title">Holding Period</div>
  481. <div class="chart-sub">Distribuzione dei giorni di permanenza in posizione</div>
  482. <canvas id="chartHolding" style="max-height:200px;"></canvas>
  483. <div class="no-data-chart" id="nd-holding"><span>⏱</span>Nessun trade chiuso</div>
  484. </div>
  485. <div class="chart-box">
  486. <div class="chart-title">P&L per Tipo Strumento</div>
  487. <div class="chart-sub">Stock Β· ETF Β· Future β€” profitto netto realizzato</div>
  488. <canvas id="chartAsset" style="max-height:200px;"></canvas>
  489. <div class="no-data-chart" id="nd-asset"><span>πŸ“Š</span>Nessun trade chiuso</div>
  490. </div>
  491. </div>
  492.  
  493. <!-- ROW 4: Bull Put vs Bear Call + P&L Reale per trade -->
  494. <div class="grid-1-1" style="margin-bottom:20px;">
  495. <div class="chart-box">
  496. <div class="chart-title">Bull Put vs Bear Call</div>
  497. <div class="chart-sub">Avg P&L realizzato e Win Rate per tipologia di spread</div>
  498. <canvas id="chartTypeComp" style="max-height:240px;"></canvas>
  499. <div class="no-data-chart" id="nd-type"><span>βš–οΈ</span>Nessun trade chiuso</div>
  500. </div>
  501. <div class="chart-box">
  502. <div class="chart-title">P&L Realizzato per Trade</div>
  503. <div class="chart-sub">Ogni barra = un trade chiuso, in ordine cronologico</div>
  504. <canvas id="chartPnlBar" style="max-height:240px;"></canvas>
  505. <div class="no-data-chart" id="nd-pnlbar"><span>πŸ’Ή</span>Nessun trade chiuso</div>
  506. </div>
  507. </div>
  508.  
  509. <!-- CALENDARIO SCADENZE (full width) -->
  510. <div class="chart-box" style="margin-bottom:20px;">
  511. <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:4px;">
  512. <div>
  513. <div class="chart-title">πŸ“… Calendario Scadenze Avanzato</div>
  514. <div class="chart-sub">Scadenze organizzate per mese con statistiche dettagliate β€” quantitΓ , tipologia, esiti, P&L</div>
  515. </div>
  516. <div class="cal-filter-bar" id="calYearFilter"></div>
  517. </div>
  518. <!-- Calendar summary KPIs -->
  519. <div class="grid-4" id="calSummaryKpis" style="margin:10px 0 14px;"></div>
  520. <div id="expiryCalendar" style="margin-top:0;"></div>
  521. </div>
  522.  
  523. <!-- STATS TABLE full width -->
  524. <div class="chart-box" style="margin-bottom:20px;">
  525. <div class="chart-title">πŸ“‹ Statistiche Avanzate</div>
  526. <div class="chart-sub">Riepilogo completo della performance</div>
  527. <div id="statsTable" style="margin-top:12px;display:grid;grid-template-columns:1fr 1fr;gap:0 40px;"></div>
  528. </div>
  529.  
  530. </div>
  531. </div><!-- end page-analytics -->
  532.  
  533. <!-- CLOSE MODAL -->
  534. <div class="modal-overlay" id="closeModal">
  535. <div class="modal">
  536. <h2>πŸ“Œ Chiudi Trade</h2>
  537. <input type="hidden" id="close-trade-id">
  538. <div class="form-grid" style="grid-template-columns:1fr 1fr;">
  539. <div class="form-group"><label>Prezzo di Chiusura (debit pagato, 0 se scaduto)</label><input type="text" inputmode="decimal" id="close-price" placeholder="es. 0,20" value="0" oninput="onClosePriceChange()" /></div>
  540. <div class="form-group"><label>Data Chiusura</label><input type="date" id="close-date" /></div>
  541. <div class="form-group"><label id="close-fees-label">Commissioni Chiusura ($) <span id="close-fees-note" style="color:var(--green);font-size:10px;">βœ“ Scadenza β€” nessuna comm.</span></label><input type="text" inputmode="decimal" id="close-fees" placeholder="es. 6,50" value="0" disabled style="opacity:.4;cursor:not-allowed;" /></div>
  542. <div class="form-group"><label>Motivo Chiusura</label>
  543. <select id="close-reason">
  544. <option value="closed">βœ… Profitto / Scadenza</option>
  545. <option value="stop">πŸ›‘ Stop Loss</option>
  546. <option value="early">↩ Chiusura Anticipata</option>
  547. </select>
  548. </div>
  549. </div>
  550. <div id="closePreview" style="background:var(--surface2);border-radius:8px;padding:12px;margin:14px 0;font-size:13px;"></div>
  551. <div style="display:flex;gap:10px;margin-top:16px;">
  552. <button class="btn btn-success" onclick="confirmClose()">Conferma Chiusura</button>
  553. <button class="btn btn-outline" onclick="closeModal()">Annulla</button>
  554. </div>
  555. </div>
  556. </div>
  557.  
  558. <script>
  559. // ─── STATE ───────────────────────────────────────────────────────────────────
  560. let trades = JSON.parse(localStorage.getItem('vst_trades') || '[]');
  561. let nextId = parseInt(localStorage.getItem('vst_nextid') || '1');
  562.  
  563. function save() {
  564. localStorage.setItem('vst_trades', JSON.stringify(trades));
  565. localStorage.setItem('vst_nextid', nextId);
  566. flashAutosave();
  567. }
  568.  
  569. // ─── TOAST NOTIFICATION ───────────────────────────────────────────────────────
  570. let toastTimer = null;
  571. function showToast(msg, type = 'success', duration = 3000) {
  572. const el = document.getElementById('toast');
  573. el.textContent = msg;
  574. el.className = `toast ${type} show`;
  575. clearTimeout(toastTimer);
  576. toastTimer = setTimeout(() => el.classList.remove('show'), duration);
  577. }
  578.  
  579. // ─── AUTO-SAVE FLASH ──────────────────────────────────────────────────────────
  580. function flashAutosave() {
  581. const dot = document.querySelector('.autosave-dot');
  582. if (!dot) return;
  583. dot.style.background = '#f9c74f';
  584. setTimeout(() => dot.style.background = '#00d395', 800);
  585. }
  586.  
  587. // ─── EXPORT JSON (BACKUP COMPLETO) ────────────────────────────────────────────
  588. function exportJSON() {
  589. if (trades.length === 0) {
  590. showToast('⚠ Nessun trade da salvare!', 'error'); return;
  591. }
  592. const backup = {
  593. version: 1,
  594. exportDate: new Date().toISOString(),
  595. nextId,
  596. trades
  597. };
  598. const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
  599. const a = document.createElement('a');
  600. a.href = URL.createObjectURL(blob);
  601. a.download = `VST_backup_${today()}.json`;
  602. a.click();
  603. URL.revokeObjectURL(a.href);
  604. showToast(`πŸ’Ύ Backup salvato β€” ${trades.length} trade esportati!`, 'success');
  605. }
  606.  
  607. // ─── IMPORT JSON (RIPRISTINO) ─────────────────────────────────────────────────
  608. function importJSON() {
  609. // Chiede conferma solo se ci sono giΓ  dati
  610. if (trades.length > 0) {
  611. if (!confirm(`⚠ Attenzione!\n\nHai giΓ  ${trades.length} trade caricati.\n\nScegli:\nβ€’ OK = Sostituisci tutto con il backup\nβ€’ Annulla = Aggiungi il backup ai trade esistenti\n\n(Premi OK per sostituire, Annulla per unire)`)) {
  612. document.getElementById('jsonFileInput').setAttribute('data-mode', 'merge');
  613. } else {
  614. document.getElementById('jsonFileInput').setAttribute('data-mode', 'replace');
  615. }
  616. } else {
  617. document.getElementById('jsonFileInput').setAttribute('data-mode', 'replace');
  618. }
  619. document.getElementById('jsonFileInput').value = ''; // reset per permettere di ricaricare lo stesso file
  620. document.getElementById('jsonFileInput').click();
  621. }
  622.  
  623. function loadJSONFile(event) {
  624. const file = event.target.files[0];
  625. if (!file) return;
  626. const mode = document.getElementById('jsonFileInput').getAttribute('data-mode') || 'replace';
  627. const reader = new FileReader();
  628. reader.onload = function(e) {
  629. try {
  630. const backup = JSON.parse(e.target.result);
  631. // Supporta sia il formato con wrapper { version, trades } che un array diretto
  632. const incoming = Array.isArray(backup) ? backup : (backup.trades || []);
  633. const incomingNextId = backup.nextId || null;
  634.  
  635. if (!incoming.length) {
  636. showToast('⚠ File JSON vuoto o non valido.', 'error'); return;
  637. }
  638.  
  639. if (mode === 'replace') {
  640. trades = incoming;
  641. nextId = incomingNextId || (Math.max(...incoming.map(t => t.id || 0)) + 1);
  642. } else {
  643. // Merge: evita duplicati per ID
  644. const existingIds = new Set(trades.map(t => t.id));
  645. const toAdd = incoming.filter(t => !existingIds.has(t.id));
  646. trades = [...trades, ...toAdd];
  647. nextId = Math.max(nextId, ...[...trades.map(t => t.id || 0)]) + 1;
  648. }
  649.  
  650. save();
  651. renderAll();
  652. const modeLabel = mode === 'replace' ? 'sostituiti' : 'uniti';
  653. showToast(`βœ… ${incoming.length} trade ${modeLabel} con successo!`, 'info', 4000);
  654. } catch(err) {
  655. showToast('❌ Errore: file JSON non valido o corrotto.', 'error', 5000);
  656. }
  657. };
  658. reader.readAsText(file);
  659. }
  660.  
  661. // ─── INIT ─────────────────────────────────────────────────────────────────────
  662. document.addEventListener('DOMContentLoaded', () => {
  663. document.getElementById('f-open-date').value = today();
  664. document.getElementById('close-date').value = today();
  665. renderAll();
  666. });
  667.  
  668. function today() {
  669. return new Date().toISOString().split('T')[0];
  670. }
  671.  
  672. // ─── EU NUMBER FORMATTING ─────────────────────────────────────────────────────
  673. // Decimali con virgola, migliaia con punto (es: 1.234,56)
  674. function euFmt(n, decimals = 2) {
  675. return n.toLocaleString('it-IT', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
  676. }
  677. function euCurr(n) {
  678. return '$' + euFmt(n, 2);
  679. }
  680. // Accetta sia "0.26" che "0,26" β€” fondamentale per utenti con tastiera europea
  681. function parseEU(str) {
  682. if (str === null || str === undefined) return NaN;
  683. const s = String(str).trim();
  684. // Se contiene sia punto che virgola: il punto Γ¨ migliaia, la virgola Γ¨ decimale
  685. // Es: "1.234,56" β†’ "1234.56"
  686. if (s.includes(',') && s.includes('.')) {
  687. return parseFloat(s.replace(/\./g, '').replace(',', '.'));
  688. }
  689. // Se contiene solo virgola: Γ¨ il decimale
  690. // Es: "0,26" β†’ "0.26"
  691. if (s.includes(',')) {
  692. return parseFloat(s.replace(',', '.'));
  693. }
  694. // Altrimenti Γ¨ giΓ  formato JS standard con punto
  695. // Es: "1.4", "300", "6.5"
  696. return parseFloat(s);
  697. }
  698.  
  699. // Converte un numero JS in stringa EU per i campi del form
  700. // Es: 1.4 β†’ "1,4" | 300 β†’ "300" | 6.5 β†’ "6,5"
  701. function toField(n, decimals) {
  702. if (n === null || n === undefined || isNaN(n)) return '';
  703. if (decimals !== undefined) {
  704. return euFmt(n, decimals);
  705. }
  706. // Decimali automatici: mostra solo i decimali necessari
  707. const s = n.toString();
  708. const parts = s.split('.');
  709. if (parts.length === 1) return s; // numero intero, nessuna virgola
  710. return parts[0] + ',' + parts[1]; // sostituisce il punto con virgola
  711. }
  712.  
  713. // ─── ASSET TYPE β†’ AUTO MOLTIPLICATORE ────────────────────────────────────────
  714. const MULT_MAP = { stock:100, etf:100, future_index:1, future_mp:1000, future_fx:100000 };
  715. function onAssetTypeChange() {
  716. const at = document.getElementById('f-asset-type').value;
  717. document.getElementById('f-multiplier').value = MULT_MAP[at] || 100;
  718. calcPreview();
  719. }
  720.  
  721. // ─── SPREAD TYPE ──────────────────────────────────────────────────────────────
  722. function setSpreadType(type, btn) {
  723. document.getElementById('spreadType').value = type;
  724. document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  725. btn.classList.add('active');
  726. const isBullPut = type === 'bull_put';
  727. document.getElementById('sell-strike-lbl').textContent = isBullPut ? 'Put VENDUTA (OTM)' : 'Call VENDUTA (OTM)';
  728. document.getElementById('buy-strike-lbl').textContent = isBullPut ? 'Put COMPRATA (piΓΉ bassa)' : 'Call COMPRATA (piΓΉ alta)';
  729. validateStrikesLive();
  730. calcPreview();
  731. }
  732.  
  733. // ─── LIVE STRIKE VALIDATION ───────────────────────────────────────────────────
  734. // PuΓ² essere chiamata senza parametri (aggiorna il DOM) o con valori giΓ  parsati
  735. // (usata da saveTrade per validare senza toccare il DOM state)
  736. function validateStrikesLive(sellVal, buyVal, typeVal) {
  737. const sell = sellVal !== undefined ? sellVal : parseFloat(document.getElementById('f-sell-strike').value);
  738. const buy = buyVal !== undefined ? buyVal : parseFloat(document.getElementById('f-buy-strike').value);
  739. const type = typeVal !== undefined ? typeVal : document.getElementById('spreadType').value;
  740. const buyEl = document.getElementById('f-buy-strike');
  741. const errEl = document.getElementById('strikeError');
  742.  
  743. // Se uno dei due campi Γ¨ vuoto/NaN β†’ nessun errore, lascia passare
  744. if (isNaN(sell) || isNaN(buy) || sell === 0 || buy === 0) {
  745. buyEl.classList.remove('input-error');
  746. errEl.style.display = 'none';
  747. return true;
  748. }
  749.  
  750. // Bull Put Spread: Put comprata DEVE essere INFERIORE alla Put venduta
  751. // Bear Call Spread: Call comprata DEVE essere SUPERIORE alla Call venduta
  752. const invalid = (type === 'bull_put' && buy >= sell) || (type === 'bear_call' && buy <= sell);
  753.  
  754. // Aggiorna il DOM solo se chiamata senza parametri (dalla UI)
  755. if (sellVal === undefined) {
  756. buyEl.classList.toggle('input-error', invalid);
  757. errEl.style.display = invalid ? 'block' : 'none';
  758. }
  759.  
  760. return !invalid;
  761. }
  762.  
  763. // ─── CALC PREVIEW ─────────────────────────────────────────────────────────────
  764. function calcPreview() {
  765. const prem = parseEU(document.getElementById('f-premium').value) || 0;
  766. const sell = parseEU(document.getElementById('f-sell-strike').value) || 0;
  767. const buy = parseEU(document.getElementById('f-buy-strike').value) || 0;
  768. const cont = parseInt(document.getElementById('f-contracts').value) || 1;
  769. const mult = parseEU(document.getElementById('f-multiplier').value) || 100;
  770. const broker = parseEU(document.getElementById('f-margin-broker').value) || 0;
  771. const fees = parseEU(document.getElementById('f-fees').value) || 0;
  772. if (!prem || !sell || !buy) return;
  773.  
  774. // 1) Max Profitto Netto = (premio Γ— molt Γ— cont) βˆ’ commissioni
  775. // Es: 0,26 Γ— 100 Γ— 5 = 130 βˆ’ 6,50 comm = 123,50
  776. const grossPremium = prem * mult * cont;
  777. const maxProfit = grossPremium - fees;
  778.  
  779. // 2) Stop Loss debit totale = premio Γ— 3 Γ— molt Γ— cont
  780. // Es: 0,26 Γ— 3 Γ— 100 Γ— 5 = 390,00
  781. const stopDebit = prem * 3 * mult * cont;
  782.  
  783. // 3) Max Perdita con SL = stopDebit + commissioni βˆ’ maxProfittoNetto
  784. // Es: 390 + 6,50 βˆ’ 123,50 = 273,00
  785. const maxLossWithSL = stopDebit + fees - maxProfit;
  786.  
  787. // 4) R/R = MaxPerditaSL / MaxProfittoNetto (quante volte rischio vs guadagno)
  788. // poi si toglie 1 per avere il rapporto puro
  789. // Es: 273 / 123,50 = 2,21 β†’ mostro "1 : 2,21"
  790. const rrRaw = maxLossWithSL > 0 && maxProfit > 0 ? (maxLossWithSL / maxProfit) : null;
  791. const rrStr = rrRaw !== null ? `1 : ${euFmt(rrRaw, 2)}` : 'β€”';
  792.  
  793. // 5) ROI = MaxProfittoNetto / margine broker Γ— 100
  794. const roi = broker > 0 ? euFmt((maxProfit / broker) * 100, 2) + '%' : 'β€”';
  795.  
  796. document.getElementById('cp-maxprofit').textContent = euCurr(maxProfit);
  797. document.getElementById('cp-maxloss').textContent = '-' + euCurr(maxLossWithSL);
  798. document.getElementById('cp-rr').textContent = rrStr;
  799. document.getElementById('cp-roi').textContent = roi;
  800. }
  801.  
  802. // ─── SAVE TRADE ───────────────────────────────────────────────────────────────
  803. function saveTrade() {
  804. const ticker = document.getElementById('f-ticker').value.trim().toUpperCase();
  805. const openDate = document.getElementById('f-open-date').value;
  806. const expiry = document.getElementById('f-expiry').value;
  807. const contracts = parseInt(document.getElementById('f-contracts').value) || 1;
  808. const mult = parseEU(document.getElementById('f-multiplier').value) || 100;
  809. const brokerMargin= parseEU(document.getElementById('f-margin-broker').value) || 0;
  810. const sellStr = parseEU(document.getElementById('f-sell-strike').value);
  811. const buyStr = parseEU(document.getElementById('f-buy-strike').value);
  812. const prem = parseEU(document.getElementById('f-premium').value);
  813. const fees = parseEU(document.getElementById('f-fees').value) || 0;
  814. const type = document.getElementById('spreadType').value;
  815. const assetType = document.getElementById('f-asset-type').value;
  816. const editId = document.getElementById('editId').value;
  817.  
  818. const alertEl = document.getElementById('formAlert');
  819. alertEl.className = 'alert';
  820.  
  821. if (!ticker || !openDate || !expiry || isNaN(sellStr) || isNaN(buyStr) || isNaN(prem)) {
  822. alertEl.textContent = '⚠ Compila tutti i campi obbligatori.';
  823. alertEl.className = 'alert error'; return;
  824. }
  825. if (prem <= 0) { alertEl.textContent = '⚠ Il premio deve essere > 0.'; alertEl.className = 'alert error'; return; }
  826. // Valida gli strike passando i valori giΓ  letti β€” nessuna dipendenza dal DOM state
  827. if (!validateStrikesLive(sellStr, buyStr, type)) {
  828. alertEl.textContent = '⚠ Strike non valido: per Bull Put la Put comprata deve essere inferiore. Per Bear Call la Call comprata deve essere superiore.';
  829. alertEl.className = 'alert error'; return;
  830. }
  831.  
  832. const spread = Math.abs(sellStr - buyStr);
  833. const spreadVal = spread * mult * contracts;
  834. const grossPremium = prem * mult * contracts;
  835. const maxProfit = grossPremium - fees; // netto commissioni
  836. const stopDebit = prem * 3 * mult * contracts; // SL debit
  837. const maxLoss = stopDebit + fees - maxProfit; // perdita effettiva con SL
  838. const stopLoss = stopDebit; // soglia debit da monitorare
  839. const margin = brokerMargin > 0 ? brokerMargin : spreadVal;
  840.  
  841. const trade = { id: editId ? parseInt(editId) : nextId++,
  842. ticker, type, assetType, openDate, expiry, contracts, multiplier: mult,
  843. sellStrike: sellStr, buyStrike: buyStr, spread, premium: prem, fees,
  844. brokerMargin, margin, maxProfit, maxLoss, stopLoss, status: 'open',
  845. closeDate: null, closePrice: null, closeFees: 0, pnl: null, closeReason: null };
  846.  
  847. if (editId) {
  848. const idx = trades.findIndex(t => t.id === parseInt(editId));
  849. if (idx > -1) { trade.status = trades[idx].status; trade.closeDate = trades[idx].closeDate;
  850. trade.closePrice = trades[idx].closePrice; trade.pnl = trades[idx].pnl;
  851. trade.closeFees = trades[idx].closeFees; trade.closeReason = trades[idx].closeReason;
  852. trades[idx] = trade; }
  853. } else { trades.unshift(trade); }
  854.  
  855. save();
  856. alertEl.textContent = editId ? 'βœ“ Trade aggiornato!' : 'βœ“ Trade salvato!';
  857. alertEl.className = 'alert success';
  858. setTimeout(() => alertEl.className = 'alert', 2500);
  859. resetForm();
  860. renderAll();
  861. }
  862.  
  863. // ─── RESET FORM ───────────────────────────────────────────────────────────────
  864. function resetForm() {
  865. ['f-ticker','f-sell-strike','f-buy-strike','f-premium','f-margin-broker'].forEach(id => document.getElementById(id).value = '');
  866. document.getElementById('f-contracts').value = 1;
  867. document.getElementById('f-fees').value = 0;
  868. document.getElementById('f-multiplier').value = 100;
  869. document.getElementById('f-asset-type').value = 'stock';
  870. document.getElementById('f-open-date').value = today();
  871. document.getElementById('f-expiry').value = '';
  872. document.getElementById('editId').value = '';
  873. document.getElementById('strikeError').style.display = 'none';
  874. document.getElementById('f-buy-strike').classList.remove('input-error');
  875. ['cp-maxprofit','cp-maxloss','cp-rr','cp-roi'].forEach(id => document.getElementById(id).textContent = 'β€”');
  876. }
  877.  
  878. // ─── EDIT TRADE ───────────────────────────────────────────────────────────────
  879. function editTrade(id) {
  880. const t = trades.find(x => x.id === id); if (!t) return;
  881. document.getElementById('editId').value = t.id;
  882. document.getElementById('f-ticker').value = t.ticker;
  883. document.getElementById('f-open-date').value = t.openDate;
  884. document.getElementById('f-expiry').value = t.expiry;
  885. document.getElementById('f-contracts').value = t.contracts;
  886. document.getElementById('f-multiplier').value = toField(t.multiplier || 100);
  887. document.getElementById('f-margin-broker').value = t.brokerMargin ? toField(t.brokerMargin) : '';
  888. document.getElementById('f-sell-strike').value = toField(t.sellStrike);
  889. document.getElementById('f-buy-strike').value = toField(t.buyStrike);
  890. document.getElementById('f-premium').value = toField(t.premium);
  891. document.getElementById('f-fees').value = toField(t.fees || 0);
  892. document.getElementById('spreadType').value = t.type;
  893. document.getElementById('f-asset-type').value = t.assetType || 'stock';
  894. document.querySelectorAll('.tab').forEach(tab => {
  895. tab.classList.toggle('active', (t.type === 'bull_put' && tab.textContent.includes('Bull')) || (t.type === 'bear_call' && tab.textContent.includes('Bear')));
  896. });
  897. validateStrikesLive();
  898. calcPreview();
  899. window.scrollTo({ top: 200, behavior: 'smooth' });
  900. }
  901.  
  902. // ─── DELETE TRADE ─────────────────────────────────────────────────────────────
  903. function deleteTrade(id) {
  904. if (!confirm('Eliminare questo trade?')) return;
  905. trades = trades.filter(t => t.id !== id);
  906. save(); renderAll();
  907. }
  908.  
  909. // ─── RIAPRI TRADE ─────────────────────────────────────────────────────────────
  910. function reopenTrade(id) {
  911. const t = trades.find(x => x.id === id);
  912. if (!t) return;
  913. const label = { closed:'Chiuso', stop:'Stop Loss', early:'Uscita Anticipata' }[t.status] || t.status;
  914. if (!confirm(`Riaprire questo trade?\n\nStato attuale: ${label}\nP&L registrato: ${t.pnl !== null ? '$'+t.pnl.toFixed(2) : 'β€”'}\n\nConfermando, il trade tornerΓ  "Aperto" e tutti i dati di chiusura saranno azzerati.`)) return;
  915. t.status = 'open';
  916. t.closeDate = null;
  917. t.closePrice = null;
  918. t.closeFees = 0;
  919. t.pnl = null;
  920. t.closeReason = null;
  921. save();
  922. renderAll();
  923. showToast('↩ Trade riaperto β€” ora puoi chiuderlo correttamente.', 'info');
  924. }
  925.  
  926. // ─── OPEN CLOSE MODAL ─────────────────────────────────────────────────────────
  927. function openModal() { window.scrollTo({ top: 200, behavior:'smooth'}); }
  928. function openCloseModal(id) {
  929. const t = trades.find(x => x.id === id); if (!t) return;
  930. const mult = t.multiplier || 100;
  931. const grossPremium = t.premium * mult * t.contracts;
  932. document.getElementById('close-trade-id').value = id;
  933. document.getElementById('close-date').value = today();
  934. document.getElementById('close-price').value = '0';
  935. document.getElementById('close-fees').value = '0';
  936. const prev = document.getElementById('closePreview');
  937. prev.innerHTML = `
  938. <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;font-size:13px;">
  939. <div><span style="color:var(--muted);font-size:10px;display:block;">TICKER / TIPO</span>
  940. <b>${t.ticker}</b> β€” ${t.type === 'bull_put' ? 'Bull Put' : 'Bear Call'}</div>
  941. <div><span style="color:var(--muted);font-size:10px;display:block;">PREMIO LORDO INCASSATO</span>
  942. <b style="color:var(--green)">${euCurr(grossPremium)}</b></div>
  943. <div><span style="color:var(--muted);font-size:10px;display:block;">STOP LOSS DEBIT SOGLIA</span>
  944. <b style="color:var(--red)">${euCurr(t.stopLoss)}</b> (riacquisto a ${euFmt(t.premium * 3, 4)})</div>
  945. <div><span style="color:var(--muted);font-size:10px;display:block;">COMM. APERTURA GIΓ€ PAGATE</span>
  946. <b>${euCurr(t.fees || 0)}</b></div>
  947. <div><span style="color:var(--muted);font-size:10px;display:block;">MOLTIPLICATORE</span>
  948. <b>${mult}</b></div>
  949. <div><span style="color:var(--muted);font-size:10px;display:block;">CONTRATTI</span>
  950. <b>${t.contracts}</b></div>
  951. </div>
  952. <div style="margin-top:10px;padding-top:8px;border-top:1px solid var(--border);font-size:11px;color:var(--muted);">
  953. P&amp;L = Premio lordo βˆ’ (Prezzo riacquisto Γ— ${mult} Γ— ${t.contracts}) βˆ’ Comm. apertura βˆ’ Comm. chiusura
  954. </div>`;
  955. document.getElementById('closeModal').classList.add('active');
  956. onClosePriceChange(); // imposta subito lo stato corretto del campo commissioni
  957. }
  958. function closeModal() { document.getElementById('closeModal').classList.remove('active'); }
  959.  
  960. function onClosePriceChange() {
  961. const price = parseEU(document.getElementById('close-price').value) || 0;
  962. const feesInput = document.getElementById('close-fees');
  963. const noteEl = document.getElementById('close-fees-note');
  964. if (price === 0) {
  965. // Scaduto a zero: nessun riacquisto β†’ nessuna commissione di chiusura
  966. feesInput.value = '0';
  967. feesInput.disabled = true;
  968. feesInput.style.opacity = '.4';
  969. feesInput.style.cursor = 'not-allowed';
  970. noteEl.textContent = 'βœ“ Scadenza β€” nessuna comm.';
  971. noteEl.style.color = 'var(--green)';
  972. } else {
  973. // Riacquisto attivo: commissioni di chiusura applicabili
  974. feesInput.disabled = false;
  975. feesInput.style.opacity = '1';
  976. feesInput.style.cursor = 'text';
  977. noteEl.textContent = '(riacquisto attivo)';
  978. noteEl.style.color = 'var(--muted)';
  979. }
  980. }
  981.  
  982. function confirmClose() {
  983. const id = parseInt(document.getElementById('close-trade-id').value);
  984. const closePrice = parseEU(document.getElementById('close-price').value) || 0;
  985. const closeDate = document.getElementById('close-date').value;
  986. const closeFees = parseEU(document.getElementById('close-fees').value) || 0;
  987. const reason = document.getElementById('close-reason').value;
  988. const t = trades.find(x => x.id === id); if (!t) return;
  989.  
  990. const mult = t.multiplier || 100;
  991.  
  992. // P&L = Premio lordo incassato βˆ’ debit pagato per riacquisto βˆ’ commissioni apertura βˆ’ commissioni chiusura
  993. const grossPremium = t.premium * mult * t.contracts;
  994. const buybackCost = closePrice * mult * t.contracts;
  995. const pnl = grossPremium - buybackCost - (t.fees || 0) - closeFees;
  996.  
  997. t.closeDate = closeDate;
  998. t.closePrice = closePrice;
  999. t.closeFees = closeFees;
  1000. t.pnl = pnl;
  1001. t.status = reason;
  1002. t.closeReason = reason;
  1003. save(); renderAll(); closeModal();
  1004. showToast(`βœ” Trade chiuso β€” P&L: ${pnl >= 0 ? '+' : ''}${euCurr(pnl)}`, pnl >= 0 ? 'success' : 'error', 4000);
  1005. }
  1006.  
  1007. // ─── RENDER CARDS ─────────────────────────────────────────────────────────────
  1008. function renderTable() {
  1009. const filterSt = document.getElementById('filterStatus').value;
  1010. const filterTy = document.getElementById('filterType').value;
  1011. const list = document.getElementById('tradeList');
  1012. list.innerHTML = '';
  1013.  
  1014. const filtered = trades.filter(t =>
  1015. (filterSt === 'all' || t.status === filterSt) &&
  1016. (filterTy === 'all' || t.type === filterTy)
  1017. );
  1018.  
  1019. document.getElementById('noTrades').style.display = filtered.length ? 'none' : 'block';
  1020.  
  1021. const assetLabels = { stock:'Stock', etf:'ETF', future_index:'Future Index', future_mp:'Future M.P.', future_fx:'Future Valute' };
  1022. const statusLabel = { open:'● Aperto', closed:'βœ” Chiuso', stop:'βœ– Stop Loss', early:'↩ Uscita Anticipata' };
  1023. const statusColors = { open:'var(--green)', closed:'#777', stop:'var(--red)', early:'var(--yellow)' };
  1024.  
  1025. filtered.forEach((t, i) => {
  1026. const typeLabel = t.type === 'bull_put' ? 'Bull Put Spread' : 'Bear Call Spread';
  1027. const typeBadge = t.type === 'bull_put' ? 'badge-put' : 'badge-call';
  1028. const mult = t.multiplier || 100;
  1029. const statusCol = statusColors[t.status] || statusColors.open;
  1030. const stLabel = statusLabel[t.status] || t.status;
  1031.  
  1032. // P&L display
  1033. const hasPnl = t.pnl !== null;
  1034. const pnlColor = hasPnl ? (t.pnl >= 0 ? 'var(--green)' : 'var(--red)') : 'var(--muted)';
  1035. const pnlText = hasPnl ? `${t.pnl >= 0 ? '+' : ''}${euCurr(t.pnl)}` : 'Aperto';
  1036. // ROI atteso = Max Profitto Netto / Margine Broker (fisso, usato per decidere il trade)
  1037. const roiAtteso = (t.margin > 0 && t.maxProfit != null) ? euFmt((t.maxProfit / t.margin) * 100, 2) + '%' : 'β€”';
  1038. const roiColor = 'var(--green)'; // sempre verde: Γ¨ un dato atteso, non realizzato
  1039.  
  1040. list.innerHTML += `
  1041. <div class="trade-card status-${t.status}">
  1042. <!-- LEFT: ticker + badge + stato -->
  1043. <div class="tc-left">
  1044. <div class="tc-ticker">${t.ticker}</div>
  1045. <span class="badge ${typeBadge}" style="text-align:center">${t.type === 'bull_put' ? 'Bull Put' : 'Bear Call'}</span>
  1046. <div class="tc-asset">${assetLabels[t.assetType] || 'β€”'}</div>
  1047. <div style="font-size:11px;font-weight:700;color:${statusCol};margin-top:2px;">${stLabel}</div>
  1048. </div>
  1049.  
  1050. <!-- MID: tutti i dati in griglia -->
  1051. <div style="display:flex;flex-direction:column;gap:10px;">
  1052. <div class="tc-mid">
  1053. <div class="tc-field"><span>Strike Venduto</span><strong>${euCurr(t.sellStrike)}</strong></div>
  1054. <div class="tc-field"><span>Strike Comprato</span><strong>${euCurr(t.buyStrike)}</strong></div>
  1055. <div class="tc-field"><span>Contratti Γ— Molt.</span><strong>${t.contracts} Γ— ${mult}</strong></div>
  1056. <div class="tc-field"><span>Premio Lordo</span><strong class="green">${euCurr(t.premium * mult * t.contracts)}</strong></div>
  1057. <div class="tc-field"><span>Commissioni</span><strong>${euCurr(t.fees||0)}</strong></div>
  1058. <div class="tc-field"><span>Max Profitto Netto</span><strong class="green">${euCurr(t.maxProfit)}</strong></div>
  1059. <div class="tc-field"><span>Stop Loss Debit</span><strong class="red">${euCurr(t.stopLoss)}</strong></div>
  1060. <div class="tc-field"><span>Max Perdita (con SL)</span><strong class="red">-${euCurr(t.maxLoss)}</strong></div>
  1061. <div class="tc-field"><span>Margine Broker</span><strong class="yellow">${euCurr(t.margin)}</strong></div>
  1062. <div class="tc-field"><span>R/R Ratio</span><strong>${t.maxProfit > 0 && t.maxLoss > 0 ? '1 : ' + euFmt(t.maxLoss/t.maxProfit, 2) : 'β€”'}</strong></div>
  1063. </div>
  1064. <div class="tc-divider"></div>
  1065. <div class="tc-dates">
  1066. <div class="tc-field"><span>Apertura</span><strong>${t.openDate}</strong></div>
  1067. <div class="tc-field"><span>Scadenza</span><strong>${t.expiry}</strong></div>
  1068. ${t.closeDate ? `<div class="tc-field"><span>Chiusura</span><strong>${t.closeDate}</strong></div>` : ''}
  1069. ${t.closePrice !== null && t.closePrice !== undefined ? `<div class="tc-field"><span>Prezzo Chiusura</span><strong>$${parseFloat(t.closePrice).toFixed(2)}</strong></div>` : ''}
  1070. </div>
  1071. </div>
  1072.  
  1073. <!-- RIGHT: P&L + ROI + azioni -->
  1074. <div class="tc-right">
  1075. <div class="tc-pnl" style="color:${pnlColor}">${pnlText}</div>
  1076. <div class="tc-roi" style="color:${roiColor}">ROI atteso ${roiAtteso}</div>
  1077. <div class="tc-actions">
  1078. ${t.status === 'open'
  1079. ? `<button class="btn btn-success btn-sm" onclick="openCloseModal(${t.id})">πŸ“Œ Chiudi</button>`
  1080. : `<button class="btn btn-sm" style="background:rgba(249,199,79,.15);border:1px solid var(--yellow);color:var(--yellow);" onclick="reopenTrade(${t.id})">↩ Riapri</button>`
  1081. }
  1082. <button class="btn btn-outline btn-sm" onclick="editTrade(${t.id})">✏ Modifica</button>
  1083. <button class="btn btn-danger btn-sm" onclick="deleteTrade(${t.id})">πŸ—‘</button>
  1084. </div>
  1085. </div>
  1086. </div>`;
  1087. });
  1088. }
  1089.  
  1090. // ─── RENDER KPIs ──────────────────────────────────────────────────────────────
  1091. function renderKPIs() {
  1092. const open = trades.filter(t => t.status === 'open');
  1093. const closed = trades.filter(t => t.status !== 'open');
  1094. const all = trades; // tutti: aperti + chiusi
  1095.  
  1096. // ── Margine impiegato = somma margine broker posizioni APERTE
  1097. const margineImpiegato = open.reduce((s, t) => s + (t.margin || 0), 0);
  1098.  
  1099. // ── P&L Realizzato
  1100. const totalPnl = closed.reduce((s, t) => s + (t.pnl || 0), 0);
  1101.  
  1102. // ── Win Rate: chiusi in profitto (scadenza + uscita anticipata in profitto)
  1103. const winsAll = closed.filter(t => (t.pnl || 0) > 0);
  1104. const wr = closed.length > 0 ? ((winsAll.length / closed.length) * 100).toFixed(1) : 0;
  1105.  
  1106. // ── Dettaglio Win Rate per categoria
  1107. const nExp = closed.filter(t => t.status === 'closed').length;
  1108. const nEarly = closed.filter(t => t.status === 'early').length;
  1109. const nStop = closed.filter(t => t.status === 'stop').length;
  1110. const tot = closed.length || 1;
  1111. const pct = n => ((n / tot) * 100).toFixed(1);
  1112.  
  1113. // ── ROI medio atteso = media semplice del ROI atteso di TUTTI i trade (aperti + chiusi)
  1114. // ROI atteso = maxProfit / margin Γ— 100 (giΓ  calcolato al momento del salvataggio)
  1115. const roiArr = all.filter(t => t.margin > 0 && t.maxProfit != null)
  1116. .map(t => (t.maxProfit / t.margin) * 100);
  1117. const roiAvg = roiArr.length > 0 ? (roiArr.reduce((a,b)=>a+b,0) / roiArr.length) : 0;
  1118.  
  1119. // ── AVERAGE box: medie su tutti i trade (aperti + chiusi)
  1120. const avg = (arr, fn) => arr.length > 0 ? arr.reduce((s,t)=>s+(fn(t)||0),0)/arr.length : null;
  1121.  
  1122. const avgProfit = avg(all, t => t.maxProfit);
  1123. const avgLoss = avg(all, t => t.maxLoss);
  1124. const avgMargin = avg(all, t => t.margin);
  1125. // R/R per ogni trade = maxLoss / maxProfit
  1126. const rrArr = all.filter(t => t.maxProfit > 0 && t.maxLoss > 0).map(t => t.maxLoss / t.maxProfit);
  1127. const avgRR = rrArr.length > 0 ? rrArr.reduce((a,b)=>a+b,0)/rrArr.length : null;
  1128.  
  1129. // ── Aggiorno il DOM
  1130. document.getElementById('k-open').textContent = open.length;
  1131. document.getElementById('k-margin').textContent = euCurr(margineImpiegato);
  1132. document.getElementById('k-margin').className = 'val yellow';
  1133.  
  1134. document.getElementById('k-pnl').textContent = `${totalPnl >= 0 ? '+' : ''}${euCurr(totalPnl)}`;
  1135. document.getElementById('k-pnl').className = `val ${totalPnl >= 0 ? 'green' : 'red'}`;
  1136.  
  1137. document.getElementById('k-wr').textContent = euFmt(parseFloat(wr), 1) + '%';
  1138. document.getElementById('k-wr').className = `val ${parseFloat(wr) >= 50 ? 'green' : 'red'}`;
  1139. document.getElementById('k-wr-sub').textContent = `${winsAll.length} win / ${closed.length} chiusi`;
  1140.  
  1141. document.getElementById('k-wr-exp').textContent = `${nExp} (${pct(nExp)}%)`;
  1142. document.getElementById('k-wr-early').textContent = `${nEarly} (${pct(nEarly)}%)`;
  1143. document.getElementById('k-wr-stop').textContent = `${nStop} (${pct(nStop)}%)`;
  1144.  
  1145. document.getElementById('k-roi-avg').textContent = euFmt(roiAvg, 2) + '%';
  1146. document.getElementById('k-roi-avg').className = `val ${roiAvg >= 0 ? 'green' : 'red'}`;
  1147.  
  1148. document.getElementById('k-avg-profit').textContent = avgProfit !== null ? euCurr(avgProfit) : 'β€”';
  1149. document.getElementById('k-avg-loss').textContent = avgLoss !== null ? euCurr(avgLoss) : 'β€”';
  1150. document.getElementById('k-avg-margin').textContent = avgMargin !== null ? euCurr(avgMargin) : 'β€”';
  1151. document.getElementById('k-avg-rr').textContent = avgRR !== null ? '1 : ' + euFmt(avgRR, 2) : 'β€”';
  1152. }
  1153.  
  1154. function renderAll() { renderKPIs(); renderTable(); if(document.getElementById('page-analytics').classList.contains('active')) renderAnalytics(); }
  1155.  
  1156. // ─── PAGE NAVIGATION ──────────────────────────────────────────────────────────
  1157. function showPage(id, btn) {
  1158. document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  1159. document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
  1160. document.getElementById('page-' + id).classList.add('active');
  1161. btn.classList.add('active');
  1162. if (id === 'analytics') renderAnalytics();
  1163. }
  1164.  
  1165. // ─── CHART DEFAULTS ───────────────────────────────────────────────────────────
  1166. const C = { green:'#00d395', red:'#ff4d6d', accent:'#6c63ff', yellow:'#f9c74f',
  1167. blue:'#4cc9f0', orange:'#f77f00', purple:'#b5179e', muted:'#8892b0',
  1168. surface2:'#22263a', border:'#2e3250', text:'#e2e8f0' };
  1169.  
  1170. Chart.defaults.color = C.muted;
  1171. Chart.defaults.borderColor = C.border;
  1172. Chart.defaults.font.family = "'Segoe UI', sans-serif";
  1173. Chart.defaults.font.size = 12;
  1174.  
  1175. let charts = {};
  1176. function destroyChart(id) { if(charts[id]) { charts[id].destroy(); delete charts[id]; } }
  1177.  
  1178. function noData(canvasId, ndId, show) {
  1179. document.getElementById(canvasId).style.display = show ? 'none' : 'block';
  1180. document.getElementById(ndId).style.display = show ? 'flex' : 'none';
  1181. }
  1182.  
  1183. // ─── ANALYTICS STATE ──────────────────────────────────────────────────────────
  1184. let eqFilter = 'all';
  1185.  
  1186. function setEqFilter(f, btn) {
  1187. eqFilter = f;
  1188. document.querySelectorAll('.eq-btn').forEach(b => b.classList.remove('active'));
  1189. btn.classList.add('active');
  1190. const closed = getFilteredClosed();
  1191. renderEquityCurve(closed);
  1192. }
  1193.  
  1194. function getFilteredClosed() {
  1195. const all = trades.filter(t => t.status !== 'open' && t.pnl !== null)
  1196. .sort((a,b) => new Date(a.closeDate) - new Date(b.closeDate));
  1197. if (eqFilter === 'all') return all;
  1198. const now = new Date();
  1199. const ms = { '1m':30, '3m':90, '6m':180, '1y':365 }[eqFilter] * 86400000;
  1200. return all.filter(t => t.closeDate && (now - new Date(t.closeDate)) <= ms);
  1201. }
  1202.  
  1203. // ─── ANALYTICS MASTER RENDER ──────────────────────────────────────────────────
  1204. function renderAnalytics() {
  1205. const allClosed = trades.filter(t => t.status !== 'open' && t.pnl !== null)
  1206. .sort((a,b) => new Date(a.closeDate) - new Date(b.closeDate));
  1207. const filtered = getFilteredClosed();
  1208.  
  1209. renderAnalyticsKPIs(allClosed);
  1210. renderEquityCurve(filtered);
  1211. renderMonthlyPnl(allClosed);
  1212. renderDrawdown(allClosed);
  1213. renderDonut(allClosed);
  1214. renderHolding(allClosed);
  1215. renderAssetType(allClosed);
  1216. renderTypeComp(allClosed);
  1217. renderPnlBar(allClosed);
  1218. renderExpiryCalendar();
  1219. renderStatsTable(allClosed);
  1220. }
  1221.  
  1222. // ─── ANALYTICS KPIs ───────────────────────────────────────────────────────────
  1223. function renderAnalyticsKPIs(closed) {
  1224. const totalPnl = closed.reduce((s,t)=>s+(t.pnl||0),0);
  1225. const wins = closed.filter(t=>t.pnl>0);
  1226. const losses = closed.filter(t=>t.pnl<=0);
  1227. const grossWin = wins.reduce((s,t)=>s+t.pnl,0);
  1228. const grossLoss = Math.abs(losses.reduce((s,t)=>s+t.pnl,0));
  1229. const pf = grossLoss>0 ? euFmt(grossWin/grossLoss,2) : wins.length>0 ? '∞' : 'β€”';
  1230. const avgWin = wins.length>0 ? wins.reduce((s,t)=>s+t.pnl,0)/wins.length : 0;
  1231. const avgLoss = losses.length>0 ? Math.abs(losses.reduce((s,t)=>s+t.pnl,0)/losses.length) : 0;
  1232. const avgWL = avgLoss>0 ? euFmt(avgWin/avgLoss,2) : avgWin>0 ? '∞' : 'β€”';
  1233. let peak=0, dd=0, cumul=0;
  1234. closed.forEach(t=>{ cumul+=t.pnl; if(cumul>peak) peak=cumul; const d=peak-cumul; if(d>dd) dd=d; });
  1235.  
  1236. document.getElementById('an-equity').textContent = (totalPnl>=0?'+':'')+euCurr(totalPnl);
  1237. document.getElementById('an-equity').className = `val ${totalPnl>=0?'green':'red'}`;
  1238. document.getElementById('an-equity-sub').textContent = `su ${closed.length} trade chiusi`;
  1239. document.getElementById('an-pf').textContent = pf;
  1240. document.getElementById('an-pf').className = `val ${parseFloat(String(pf).replace(',','.'))>=1?'green':'red'}`;
  1241. document.getElementById('an-dd').textContent = '-'+euCurr(dd);
  1242. document.getElementById('an-avgwl').textContent = avgWL;
  1243. document.getElementById('an-avgwl').className = `val ${parseFloat(String(avgWL).replace(',','.'))>=1?'green':'red'}`;
  1244. }
  1245.  
  1246. // ─── EQUITY CURVE (con filtro temporale) ──────────────────────────────────────
  1247. function renderEquityCurve(closed) {
  1248. destroyChart('equity');
  1249. if (!closed.length) { noData('chartEquity','nd-equity',true); return; }
  1250. noData('chartEquity','nd-equity',false);
  1251. let cum=0;
  1252. const labels = closed.map(t=>t.closeDate||t.openDate);
  1253. const data = closed.map(t=>{ cum+=t.pnl; return parseFloat(cum.toFixed(2)); });
  1254. const ptColors = data.map(v=>v>=0?C.green:C.red);
  1255. charts.equity = new Chart(document.getElementById('chartEquity'),{
  1256. type:'line',
  1257. data:{ labels, datasets:[{
  1258. label:'Equity ($)', data,
  1259. borderColor:C.accent, borderWidth:2.5,
  1260. pointBackgroundColor:ptColors, pointBorderColor:ptColors,
  1261. pointRadius:4, pointHoverRadius:7,
  1262. fill:true,
  1263. backgroundColor:(ctx)=>{
  1264. const g=ctx.chart.ctx.createLinearGradient(0,0,0,260);
  1265. g.addColorStop(0,'rgba(108,99,255,0.3)');
  1266. g.addColorStop(1,'rgba(108,99,255,0.01)');
  1267. return g;
  1268. }, tension:0.3
  1269. }]},
  1270. options:{ responsive:true, plugins:{ legend:{display:false},
  1271. tooltip:{ callbacks:{
  1272. title: ctx => `Data: ${ctx[0].label}`,
  1273. label: c => `Equity cumulativa: ${euCurr(c.raw)}`
  1274. }}},
  1275. scales:{
  1276. x:{ ticks:{maxRotation:45,font:{size:10}}, grid:{color:C.border} },
  1277. y:{ ticks:{callback:v=>euCurr(v)}, grid:{color:C.border} }
  1278. }}
  1279. });
  1280. }
  1281.  
  1282. // ─── MONTHLY P&L ──────────────────────────────────────────────────────────────
  1283. function renderMonthlyPnl(closed) {
  1284. destroyChart('monthly');
  1285. const wd=closed.filter(t=>t.closeDate);
  1286. if (!wd.length) { noData('chartMonthly','nd-monthly',true); return; }
  1287. noData('chartMonthly','nd-monthly',false);
  1288. const map={};
  1289. wd.forEach(t=>{ const k=t.closeDate.substring(0,7); map[k]=(map[k]||0)+t.pnl; });
  1290. const sorted=Object.entries(map).sort((a,b)=>a[0].localeCompare(b[0]));
  1291. // Etichette leggibili: "Gen 25" etc.
  1292. const monthNames=['Gen','Feb','Mar','Apr','Mag','Giu','Lug','Ago','Set','Ott','Nov','Dic'];
  1293. const labels=sorted.map(([k])=>{ const [y,m]=k.split('-'); return `${monthNames[parseInt(m)-1]} '${y.slice(2)}`; });
  1294. const data=sorted.map(e=>parseFloat(e[1].toFixed(2)));
  1295. charts.monthly = new Chart(document.getElementById('chartMonthly'),{
  1296. type:'bar',
  1297. data:{ labels, datasets:[{ label:'P&L Mensile', data,
  1298. backgroundColor:data.map(v=>v>=0?'rgba(0,211,149,0.75)':'rgba(255,77,109,0.75)'),
  1299. borderColor:data.map(v=>v>=0?C.green:C.red),
  1300. borderWidth:1.5, borderRadius:5 }]},
  1301. options:{ responsive:true, plugins:{ legend:{display:false},
  1302. tooltip:{callbacks:{label:c=>`P&L: ${euCurr(c.raw)}`}}},
  1303. scales:{ x:{grid:{color:C.border}}, y:{ticks:{callback:v=>euCurr(v)},grid:{color:C.border}} }}
  1304. });
  1305. }
  1306.  
  1307. // ─── DRAWDOWN ─────────────────────────────────────────────────────────────────
  1308. function renderDrawdown(closed) {
  1309. destroyChart('drawdown');
  1310. if (!closed.length) { noData('chartDrawdown','nd-drawdown',true); return; }
  1311. noData('chartDrawdown','nd-drawdown',false);
  1312. let peak=0, cum=0;
  1313. const labels=closed.map(t=>t.closeDate||t.openDate);
  1314. const data=closed.map(t=>{ cum+=t.pnl; if(cum>peak) peak=cum; return parseFloat((-(peak-cum)).toFixed(2)); });
  1315. charts.drawdown = new Chart(document.getElementById('chartDrawdown'),{
  1316. type:'line',
  1317. data:{ labels, datasets:[{ label:'Drawdown ($)', data,
  1318. borderColor:C.red, borderWidth:2,
  1319. pointRadius:3, pointBackgroundColor:C.red,
  1320. fill:true,
  1321. backgroundColor:'rgba(255,77,109,0.12)',
  1322. tension:0.3 }]},
  1323. options:{ responsive:true, plugins:{ legend:{display:false},
  1324. tooltip:{callbacks:{label:c=>`DD: ${euCurr(c.raw)}`}}},
  1325. scales:{
  1326. x:{ticks:{maxRotation:45,font:{size:10}},grid:{color:C.border}},
  1327. y:{ticks:{callback:v=>euCurr(v)},grid:{color:C.border}}
  1328. }}
  1329. });
  1330. }
  1331.  
  1332. // ─── DONUT ESITI ──────────────────────────────────────────────────────────────
  1333. function renderDonut(closed) {
  1334. destroyChart('donut');
  1335. if (!closed.length) { noData('chartDonut','nd-donut',true); return; }
  1336. noData('chartDonut','nd-donut',false);
  1337. const exp =closed.filter(t=>t.status==='closed').length;
  1338. const stops=closed.filter(t=>t.status==='stop').length;
  1339. const early=closed.filter(t=>t.status==='early').length;
  1340. charts.donut = new Chart(document.getElementById('chartDonut'),{
  1341. type:'doughnut',
  1342. data:{ labels:['βœ… Scadenza','πŸ›‘ Stop Loss','↩ Uscita Ant.'],
  1343. datasets:[{ data:[exp,stops,early],
  1344. backgroundColor:[C.green,C.red,C.yellow],
  1345. borderColor:C.surface2, borderWidth:3, hoverOffset:10 }]},
  1346. options:{ responsive:true, cutout:'65%',
  1347. plugins:{ legend:{position:'bottom',labels:{padding:14,usePointStyle:true,font:{size:11}}},
  1348. tooltip:{callbacks:{label:c=>`${c.label}: ${c.raw} (${closed.length>0?(c.raw/closed.length*100).toFixed(1):0}%)`}}
  1349. }}
  1350. });
  1351. }
  1352.  
  1353. // ─── HOLDING PERIOD ───────────────────────────────────────────────────────────
  1354. function renderHolding(closed) {
  1355. destroyChart('holding');
  1356. const wd=closed.filter(t=>t.closeDate&&t.openDate);
  1357. if (!wd.length) { noData('chartHolding','nd-holding',true); return; }
  1358. noData('chartHolding','nd-holding',false);
  1359. const b={'≀5gg':0,'6-14gg':0,'15-30gg':0,'31-60gg':0,'61-90gg':0,'90+gg':0};
  1360. wd.forEach(t=>{
  1361. const d=Math.round((new Date(t.closeDate)-new Date(t.openDate))/86400000);
  1362. if(d<=5) b['≀5gg']++; else if(d<=14) b['6-14gg']++; else if(d<=30) b['15-30gg']++;
  1363. else if(d<=60) b['31-60gg']++; else if(d<=90) b['61-90gg']++; else b['90+gg']++;
  1364. });
  1365. const pal=[C.accent,'#7c75ff',C.blue,'#74b9ff',C.yellow,C.orange];
  1366. charts.holding = new Chart(document.getElementById('chartHolding'),{
  1367. type:'bar',
  1368. data:{ labels:Object.keys(b),
  1369. datasets:[{ label:'NΒ° Trade', data:Object.values(b),
  1370. backgroundColor:pal, borderColor:pal, borderWidth:1.5, borderRadius:5 }]},
  1371. options:{ responsive:true, plugins:{legend:{display:false},
  1372. tooltip:{callbacks:{label:c=>`${c.raw} trade`}}},
  1373. scales:{ x:{grid:{color:C.border}},
  1374. y:{ticks:{stepSize:1,callback:v=>Number.isInteger(v)?v:''},grid:{color:C.border}} }}
  1375. });
  1376. }
  1377.  
  1378. // ─── P&L PER TIPO STRUMENTO ───────────────────────────────────────────────────
  1379. function renderAssetType(closed) {
  1380. destroyChart('asset');
  1381. if (!closed.length) { noData('chartAsset','nd-asset',true); return; }
  1382. noData('chartAsset','nd-asset',false);
  1383. const labels={'stock':'Stock','etf':'ETF','future_index':'Future Index','future_mp':'Future M.P.','future_fx':'Future Valute'};
  1384. const map={};
  1385. closed.forEach(t=>{ const k=labels[t.assetType]||t.assetType||'Altro'; map[k]=(map[k]||0)+t.pnl; });
  1386. const sorted=Object.entries(map).sort((a,b)=>b[1]-a[1]);
  1387. const pal=['rgba(108,99,255,0.8)','rgba(76,201,240,0.8)','rgba(249,199,79,0.8)','rgba(0,211,149,0.8)','rgba(255,77,109,0.8)'];
  1388. charts.asset = new Chart(document.getElementById('chartAsset'),{
  1389. type:'bar',
  1390. data:{ labels:sorted.map(e=>e[0]),
  1391. datasets:[{ label:'P&L ($)', data:sorted.map(e=>parseFloat(e[1].toFixed(2))),
  1392. backgroundColor:sorted.map((e,i)=>e[1]>=0?pal[i%pal.length]:'rgba(255,77,109,0.75)'),
  1393. borderRadius:6, borderWidth:1.5 }]},
  1394. options:{ responsive:true, plugins:{legend:{display:false},
  1395. tooltip:{callbacks:{label:c=>`P&L: ${euCurr(c.raw)}`}}},
  1396. scales:{ x:{grid:{color:C.border}}, y:{ticks:{callback:v=>euCurr(v)},grid:{color:C.border}} }}
  1397. });
  1398. }
  1399.  
  1400. // ─── BULL PUT vs BEAR CALL ────────────────────────────────────────────────────
  1401. function renderTypeComp(closed) {
  1402. destroyChart('typeComp');
  1403. if (!closed.length) { noData('chartTypeComp','nd-type',true); return; }
  1404. noData('chartTypeComp','nd-type',false);
  1405. const bp=closed.filter(t=>t.type==='bull_put');
  1406. const bc=closed.filter(t=>t.type==='bear_call');
  1407. const avgPnl=arr=>arr.length>0?arr.reduce((s,t)=>s+t.pnl,0)/arr.length:0;
  1408. const wr=arr=>arr.length>0?arr.filter(t=>t.pnl>0).length/arr.length*100:0;
  1409. charts.typeComp = new Chart(document.getElementById('chartTypeComp'),{
  1410. type:'bar',
  1411. data:{ labels:['Bull Put Spread','Bear Call Spread'],
  1412. datasets:[
  1413. { label:'Avg P&L ($)', data:[parseFloat(avgPnl(bp).toFixed(2)),parseFloat(avgPnl(bc).toFixed(2))],
  1414. backgroundColor:['rgba(249,199,79,0.75)','rgba(108,99,255,0.75)'],
  1415. borderColor:[C.yellow,C.accent], borderWidth:1.5, borderRadius:6, yAxisID:'y' },
  1416. { label:'Win Rate (%)', data:[parseFloat(wr(bp).toFixed(1)),parseFloat(wr(bc).toFixed(1))],
  1417. type:'line', yAxisID:'y2', tension:0.3,
  1418. borderColor:C.green, borderWidth:2.5,
  1419. pointBackgroundColor:C.green, pointRadius:7, pointHoverRadius:10,
  1420. backgroundColor:'transparent' }
  1421. ]},
  1422. options:{ responsive:true,
  1423. plugins:{legend:{position:'bottom',labels:{usePointStyle:true,padding:14}}},
  1424. scales:{
  1425. y:{position:'left',ticks:{callback:v=>euCurr(v)},grid:{color:C.border}},
  1426. y2:{position:'right',min:0,max:100,ticks:{callback:v=>v+'%'},grid:{drawOnChartArea:false}}
  1427. }}
  1428. });
  1429. }
  1430.  
  1431. // ─── P&L REALIZZATO PER TRADE (barre) ─────────────────────────────────────────
  1432. function renderPnlBar(closed) {
  1433. destroyChart('pnlbar');
  1434. if (!closed.length) { noData('chartPnlBar','nd-pnlbar',true); return; }
  1435. noData('chartPnlBar','nd-pnlbar',false);
  1436. const labels=closed.map((t,i)=>`#${i+1} ${t.closeDate||''}`);
  1437. const data=closed.map(t=>parseFloat(t.pnl.toFixed(2)));
  1438. charts.pnlbar = new Chart(document.getElementById('chartPnlBar'),{
  1439. type:'bar',
  1440. data:{ labels, datasets:[{ label:'P&L ($)', data,
  1441. backgroundColor:data.map(v=>v>=0?'rgba(0,211,149,0.75)':'rgba(255,77,109,0.75)'),
  1442. borderColor:data.map(v=>v>=0?C.green:C.red),
  1443. borderWidth:1.5, borderRadius:5 }]},
  1444. options:{ responsive:true, plugins:{legend:{display:false},
  1445. tooltip:{callbacks:{label:c=>`P&L: ${euCurr(c.raw)}`}}},
  1446. scales:{ x:{ticks:{maxRotation:45,font:{size:10}},grid:{color:C.border}},
  1447. y:{ticks:{callback:v=>euCurr(v)},grid:{color:C.border}} }}
  1448. });
  1449. }
  1450.  
  1451. // ─── CALENDARIO SCADENZE AVANZATO ─────────────────────────────────────────────
  1452. let calYearFilter = 'all';
  1453. function setCalYear(yr, btn) {
  1454. calYearFilter = yr;
  1455. document.querySelectorAll('.cal-yr-btn').forEach(b=>b.classList.remove('active'));
  1456. btn.classList.add('active');
  1457. renderExpiryCalendar();
  1458. }
  1459.  
  1460. function renderExpiryCalendar() {
  1461. const el = document.getElementById('expiryCalendar');
  1462. const kpiEl = document.getElementById('calSummaryKpis');
  1463. const filterEl = document.getElementById('calYearFilter');
  1464. if (!trades.length) { el.innerHTML='<div class="no-data-chart" style="height:80px"><span>πŸ“…</span>Nessun trade inserito</div>'; kpiEl.innerHTML=''; filterEl.innerHTML=''; return; }
  1465.  
  1466. const now = new Date();
  1467. const today = now.toISOString().split('T')[0];
  1468.  
  1469. // Collect years for filter
  1470. const years = [...new Set(trades.filter(t=>t.expiry).map(t=>t.expiry.substring(0,4)))].sort();
  1471. filterEl.innerHTML = `<button class="cal-yr-btn ${calYearFilter==='all'?'active':''}" onclick="setCalYear('all',this)">Tutti</button>` +
  1472. years.map(y=>`<button class="cal-yr-btn ${calYearFilter===y?'active':''}" onclick="setCalYear('${y}',this)">${y}</button>`).join('');
  1473.  
  1474. // Filter trades
  1475. const filtered = trades.filter(t => t.expiry && (calYearFilter==='all' || t.expiry.startsWith(calYearFilter)));
  1476.  
  1477. // Group by YYYY-MM
  1478. const map = {};
  1479. filtered.forEach(t => { const k=t.expiry.substring(0,7); if(!map[k]) map[k]=[]; map[k].push(t); });
  1480. const sorted = Object.entries(map).sort((a,b)=>a[0].localeCompare(b[0]));
  1481.  
  1482. // Summary KPIs
  1483. const totalExp = filtered.length;
  1484. const openExp = filtered.filter(t=>t.status==='open').length;
  1485. const closedExp = totalExp - openExp;
  1486. const totalPremium = filtered.reduce((s,t)=>s+t.premium*(t.multiplier||100)*t.contracts,0);
  1487. const closedPnl = filtered.filter(t=>t.status!=='open'&&t.pnl!=null).reduce((s,t)=>s+t.pnl,0);
  1488. const winCount = filtered.filter(t=>t.status!=='open'&&t.pnl>0).length;
  1489. const wr = closedExp>0 ? (winCount/closedExp*100).toFixed(1) : 'β€”';
  1490.  
  1491. kpiEl.innerHTML = `
  1492. <div class="kpi"><label>Scadenze Totali</label><div class="val">${totalExp}</div><div class="sub">${openExp} aperte Β· ${closedExp} chiuse</div></div>
  1493. <div class="kpi"><label>Win Rate</label><div class="val ${closedExp>0&&winCount/closedExp>=.5?'green':'red'}">${wr}%</div><div class="sub">${winCount} vinti su ${closedExp} chiusi</div></div>
  1494. <div class="kpi"><label>Premio Lordo</label><div class="val yellow">${euCurr(totalPremium)}</div><div class="sub">premio totale incassato</div></div>
  1495. <div class="kpi"><label>P&L Realizzato</label><div class="val ${closedPnl>=0?'green':'red'}">${(closedPnl>=0?'+':'')+euCurr(closedPnl)}</div><div class="sub">sui trade chiusi</div></div>`;
  1496.  
  1497. const monthNames=['Gen','Feb','Mar','Apr','Mag','Giu','Lug','Ago','Set','Ott','Nov','Dic'];
  1498. const assetShort={'stock':'STK','etf':'ETF','future_index':'IDX','future_mp':'MP','future_fx':'FX'};
  1499. const typeShort={'bull_put':'BP','bear_call':'BC'};
  1500. const typeColors={'bull_put':'var(--green)','bear_call':'var(--red)'};
  1501. const outcomeLabels={closed:'Scadenza',stop:'Stop Loss',early:'Uscita Ant.'};
  1502. const outcomeColors={closed:'rgba(142,142,142,.2)',stop:'rgba(255,71,87,.2)',early:'rgba(255,214,10,.2)'};
  1503. const outcomeFg={closed:'#999',stop:'var(--red)',early:'var(--yellow)'};
  1504.  
  1505. // Find max month pnl for bar scaling
  1506. const monthPnls = sorted.map(([,ts])=> {
  1507. const c=ts.filter(t=>t.status!=='open'&&t.pnl!=null);
  1508. return c.reduce((s,t)=>s+t.pnl,0);
  1509. });
  1510. const maxAbsPnl = Math.max(...monthPnls.map(Math.abs), 1);
  1511.  
  1512. el.innerHTML = `<div class="cal-grid">${sorted.map(([ym, ts], idx) => {
  1513. const [y,m] = ym.split('-');
  1514. const mi = parseInt(m)-1;
  1515. const monthLabel = `${monthNames[mi]} ${y}`;
  1516. const isPast = ym < today.substring(0,7);
  1517. const isCur = ym === today.substring(0,7);
  1518. const openC = ts.filter(t=>t.status==='open').length;
  1519. const closedC = ts.filter(t=>t.status!=='open').length;
  1520.  
  1521. // Type breakdown
  1522. const bpCount = ts.filter(t=>t.type==='bull_put').length;
  1523. const bcCount = ts.filter(t=>t.type==='bear_call').length;
  1524. const total = ts.length;
  1525. const bpPct = total>0 ? (bpCount/total*100).toFixed(0) : 0;
  1526. const bcPct = total>0 ? (bcCount/total*100).toFixed(0) : 0;
  1527.  
  1528. // Outcome breakdown (closed only)
  1529. const closedTrades = ts.filter(t=>t.status!=='open');
  1530. const outcomes = {};
  1531. closedTrades.forEach(t=>{ outcomes[t.status]=(outcomes[t.status]||0)+1; });
  1532.  
  1533. // P&L for month
  1534. const mPnl = closedTrades.filter(t=>t.pnl!=null).reduce((s,t)=>s+t.pnl,0);
  1535. const pnlPct = Math.min(Math.abs(mPnl)/maxAbsPnl*100, 100);
  1536. const pnlColor = mPnl>=0 ? 'var(--green)' : 'var(--red)';
  1537.  
  1538. // Contracts total
  1539. const totalContracts = ts.reduce((s,t)=>s+t.contracts,0);
  1540.  
  1541. // Trade rows
  1542. const rows = ts.sort((a,b)=>a.expiry.localeCompare(b.expiry)).map(t => {
  1543. const dte = Math.ceil((new Date(t.expiry)-now)/86400000);
  1544. const isSoon = dte>=0 && dte<=7 && t.status==='open';
  1545. const isExp = t.expiry < today;
  1546. const rc = isExp&&t.status!=='open' ? 'cal-past' : isSoon ? 'cal-soon' : '';
  1547. const dot = {open:'<span style="color:var(--green);font-weight:700;">●</span>',closed:'<span style="color:#666;">βœ”</span>',stop:'<span style="color:var(--red);">βœ–</span>',early:'<span style="color:var(--yellow);">↩</span>'}[t.status]||'';
  1548. const roi = t.margin>0&&t.maxProfit!=null ? `ROI ${euFmt(t.maxProfit/t.margin*100,1)}%` : '';
  1549. const ed = t.expiry.split('-')[2];
  1550. const ds = t.status==='open' ? (dte<0?'scaduta':`${dte}gg`) : (t.pnl!=null?(t.pnl>=0?'+':'')+euCurr(t.pnl):'');
  1551. const dsColor = t.status==='open' ? (dte<0?'var(--muted)':dte<=7?'var(--yellow)':'var(--accent)') : (t.pnl>=0?'var(--green)':'var(--red)');
  1552. return `<div class="cal-trade-row ${rc}">
  1553. <div class="cal-trade-left">
  1554. <div class="cal-trade-ticker">${dot} ${t.ticker} <span style="color:var(--muted);font-size:10px;font-weight:400;">${assetShort[t.assetType]||''} Β· ${typeShort[t.type]||''}</span></div>
  1555. <div class="cal-trade-info">Scad. ${ed}/${m} Β· ${t.contracts}ct Β· ${euFmt(t.premium,2)} prem.</div>
  1556. </div>
  1557. <div class="cal-trade-right">
  1558. <span style="color:var(--green);font-size:11px;font-weight:700;">${roi}</span>
  1559. <span style="color:${dsColor};font-size:11px;font-weight:600;">${ds}</span>
  1560. </div>
  1561. </div>`;
  1562. }).join('');
  1563.  
  1564. const hBg = isPast ? 'var(--surface2)' : isCur ? 'rgba(108,99,255,0.15)' : 'rgba(76,201,240,0.08)';
  1565. const outcomePills = Object.entries(outcomes).map(([k,v])=>`<span class="cal-outcome-pill" style="background:${outcomeColors[k]||'rgba(142,142,142,.1)'};color:${outcomeFg[k]||'#888'};">${outcomeLabels[k]||k} ${v}</span>`).join('');
  1566.  
  1567. return `<div class="cal-month">
  1568. <div class="cal-month-header" style="background:${hBg};">
  1569. <span>${monthLabel} ${isCur?'<span style="font-size:10px;color:var(--accent);">← ora</span>':isPast?'<span style="font-size:10px;color:var(--muted);">(passato)</span>':''}</span>
  1570. <span class="cal-count">${total} trade</span>
  1571. </div>
  1572. <div class="cal-month-stats">
  1573. <div class="cal-ms"><div class="cal-ms-val">${openC}</div><div class="cal-ms-lbl">Aperti</div></div>
  1574. <div class="cal-ms"><div class="cal-ms-val">${closedC}</div><div class="cal-ms-lbl">Chiusi</div></div>
  1575. <div class="cal-ms"><div class="cal-ms-val">${totalContracts}</div><div class="cal-ms-lbl">Contratti</div></div>
  1576. </div>
  1577. <div class="cal-type-bar">
  1578. <div class="cal-type-seg" style="width:${bpPct}%;background:var(--green);" title="Bull Put ${bpPct}%"></div>
  1579. <div class="cal-type-seg" style="width:${bcPct}%;background:var(--red);" title="Bear Call ${bcPct}%"></div>
  1580. </div>
  1581. ${outcomePills?`<div class="cal-outcome-row">${outcomePills}</div>`:''}
  1582. <div class="cal-pnl-bar">
  1583. <div class="cal-pnl-track"><div class="cal-pnl-fill" style="width:${pnlPct}%;background:${pnlColor};"></div></div>
  1584. <div class="cal-pnl-val" style="color:${pnlColor};">${closedC>0?(mPnl>=0?'+':'')+euCurr(mPnl):'β€”'}</div>
  1585. </div>
  1586. ${rows}
  1587. </div>`;
  1588. }).join('')}</div>`;
  1589. }
  1590.  
  1591. // ─── STATISTICHE AVANZATE ─────────────────────────────────────────────────────
  1592. function renderStatsTable(closed) {
  1593. const el = document.getElementById('statsTable');
  1594. if (!closed.length) { el.innerHTML='<div class="no-data-chart" style="height:80px;"><span>πŸ“‹</span>Nessun dato ancora</div>'; return; }
  1595. const wins = closed.filter(t=>t.pnl>0);
  1596. const losses = closed.filter(t=>t.pnl<=0);
  1597. const pnlArr = closed.map(t=>t.pnl);
  1598. const maxWin = Math.max(...pnlArr);
  1599. const maxLoss = Math.min(...pnlArr);
  1600. const totalPnl = closed.reduce((s,t)=>s+t.pnl,0);
  1601. const grossWin = wins.reduce((s,t)=>s+t.pnl,0);
  1602. const grossLoss= Math.abs(losses.reduce((s,t)=>s+t.pnl,0));
  1603. const avgWin = wins.length>0 ? grossWin/wins.length : 0;
  1604. const avgLoss = losses.length>0 ? grossLoss/losses.length : 0;
  1605. const daysArr = closed.filter(t=>t.closeDate).map(t=>Math.round((new Date(t.closeDate)-new Date(t.openDate))/86400000));
  1606. const avgDays = daysArr.length>0 ? Math.round(daysArr.reduce((a,b)=>a+b,0)/daysArr.length) : 0;
  1607. const maxDays = daysArr.length>0 ? Math.max(...daysArr) : 0;
  1608. const roiArr = closed.filter(t=>t.margin>0&&t.maxProfit!=null).map(t=>t.maxProfit/t.margin*100);
  1609. const avgRoi = roiArr.length>0 ? roiArr.reduce((a,b)=>a+b,0)/roiArr.length : 0;
  1610. const totalPrem= closed.reduce((s,t)=>s+t.premium*(t.multiplier||100)*t.contracts,0);
  1611.  
  1612. const col1=[
  1613. ['Trade chiusi totali', closed.length, ''],
  1614. ['Trade in profitto', `${wins.length} (${closed.length>0?(wins.length/closed.length*100).toFixed(1):0}%)`, 'green'],
  1615. ['Trade in perdita', `${losses.length} (${closed.length>0?(losses.length/closed.length*100).toFixed(1):0}%)`, 'red'],
  1616. ['Miglior trade', euCurr(maxWin), 'green'],
  1617. ['Peggior trade', euCurr(maxLoss), 'red'],
  1618. ['P&L cumulativo', (totalPnl>=0?'+':'')+euCurr(totalPnl), totalPnl>=0?'green':'red'],
  1619. ];
  1620. const col2=[
  1621. ['Premio lordo totale incassato', euCurr(totalPrem), 'yellow'],
  1622. ['Avg Win per trade', euCurr(avgWin), 'green'],
  1623. ['Avg Loss per trade', euCurr(avgLoss), 'red'],
  1624. ['ROI medio atteso', euFmt(avgRoi,2)+'%', 'green'],
  1625. ['Giorni medi in posizione', `${avgDays} gg`, ''],
  1626. ['Holding max', `${maxDays} gg`, ''],
  1627. ];
  1628. const renderCol = rows => rows.map(([l,v,c])=>`<div class="stat-row"><span class="stat-label">${l}</span><span class="stat-val ${c}">${v}</span></div>`).join('');
  1629. el.innerHTML = `<div>${renderCol(col1)}</div><div>${renderCol(col2)}</div>`;
  1630. }
  1631.  
  1632. // ─── EXPORT CSV ───────────────────────────────────────────────────────────────
  1633. function exportCSV() {
  1634. const headers = ['ID','Ticker','Tipo','Apertura','Scadenza','Short Strike','Long Strike','Spread','Contratti','Premio','Max Profit','Max Loss','Margine','Stop Loss','Prezzo Chiusura','Data Chiusura','P&L','ROM%','Stato'];
  1635. const rows = trades.map(t => [
  1636. t.id, t.ticker, t.type === 'bull_put' ? 'Bull Put' : 'Bear Call',
  1637. t.openDate, t.expiry, t.sellStrike, t.buyStrike, t.spread, t.contracts,
  1638. t.premium, t.maxProfit.toFixed(2), t.maxLoss.toFixed(2),
  1639. t.margin.toFixed(2), t.stopLoss.toFixed(2),
  1640. t.closePrice ?? '', t.closeDate ?? '',
  1641. t.pnl !== null ? t.pnl.toFixed(2) : '',
  1642. (t.pnl !== null && t.margin > 0) ? (t.pnl/t.margin*100).toFixed(2) : '',
  1643. t.status
  1644. ]);
  1645. const csv = [headers, ...rows].map(r => r.join(',')).join('\n');
  1646. const a = document.createElement('a');
  1647. a.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv);
  1648. a.download = `vertical_spreads_${today()}.csv`;
  1649. a.click();
  1650. }
  1651. </script>
  1652. </body>
  1653. </html>
RAW Paste Data Copied