Torn Stock Analyzer

Analyzes all 35 Torn City stocks and scores them for buy signals using 4 data-backed indicators: drop from weekly peak (dynamic volatility threshold), position in short-term range, active price rise (m30>h1>h2), and MACD momentum. Backtested on 42 days of hourly data with 88% hit rate. Includes ROI planner, benefit block tracker, swing trade P/L, and Quick Trade bar.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         Torn Stock Analyzer
// @namespace    https://greasyfork.org
// @version      2.5.1
// @author       AeC3
// @description  Analyzes all 35 Torn City stocks and scores them for buy signals using 4 data-backed indicators: drop from weekly peak (dynamic volatility threshold), position in short-term range, active price rise (m30>h1>h2), and MACD momentum. Backtested on 42 days of hourly data with 88% hit rate. Includes ROI planner, benefit block tracker, swing trade P/L, and Quick Trade bar.
// @match        https://www.torn.com/page.php?sid=stocks*
// @run-at       document-end
// @license      MIT
// @grant        GM_xmlhttpRequest
// @connect      tornsy.com
// @connect      api.torn.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
// ==/UserScript==

(function () {
  function lsGet(key, fallback) {
    try { var v = localStorage.getItem(key); return v !== null ? v : (fallback !== undefined ? fallback : ""); }
    catch(e) { return fallback !== undefined ? fallback : ""; }
  }
  function lsSet(key, value) {
    try { localStorage.setItem(key, value); } catch(e) {}
  }

  var TORN_API_KEY = lsGet("tsa-torn-apikey", "");

  function getTornKey() {
    return lsGet("tsa-torn-apikey", "");
  }

  var KEY_BUILDER_URL = "https://www.torn.com/preferences.php#tab=api?step=addNewKey&user=basic,money,stocks&faction=donations&market=itemmarket&title=TORN%20STOCK%20ANALYZER";

  function showKeyOnboarding(contentEl, onSave) {
    var isDark = document.getElementById("tsa-overlay") ? document.getElementById("tsa-overlay").classList.contains("tsa-dark") : false;
    var bg   = isDark ? "#0f0f1a" : "#ffffff";
    var bg2  = isDark ? "#1a1a2e" : "#f7f9fc";
    var text = isDark ? "#c8c8d8" : "#222";
    var muted = isDark ? "#6a6a8a" : "#888";
    var border = isDark ? "#2a2a4a" : "#e0e0e0";
    contentEl.innerHTML =
      "<div style=\"padding:18px;background:" + bg + ";font-family:sans-serif\">" +
        "<div style=\"font-size:13px;font-weight:bold;color:" + text + ";margin-bottom:6px\">API Key Required</div>" +
        "<div style=\"font-size:11px;color:" + muted + ";margin-bottom:14px;line-height:1.5\">Torn Stock Analyzer needs a Torn API key to load stock and portfolio data. Your key is stored only in your browser and sent exclusively to api.torn.com.</div>" +
        "<a href=\"" + KEY_BUILDER_URL + "\" target=\"_blank\" style=\"display:block;text-align:center;padding:10px;border-radius:8px;background:#4a6fa5;color:#fff;font-size:12px;font-weight:bold;text-decoration:none;margin-bottom:14px\">Create custom key (recommended)</a>" +
        "<div style=\"font-size:10px;color:" + muted + ";margin-bottom:6px;text-align:center\">— or enter an existing key below —</div>" +
        "<div style=\"font-size:10px;color:" + muted + ";margin-bottom:6px\">Limited Access key or custom key</div>" +
        "<input id=\"tsa-key-input\" type=\"text\" placeholder=\"Paste API key here\" style=\"width:100%;box-sizing:border-box;padding:8px 10px;border-radius:7px;border:1px solid " + border + ";background:" + bg2 + ";color:" + text + ";font-size:12px;margin-bottom:10px\">" +
        "<button id=\"tsa-key-save\" style=\"width:100%;padding:9px;border-radius:7px;border:none;background:#4a6fa5;color:#fff;font-size:13px;font-weight:bold;cursor:pointer\">Save key</button>" +
      "</div>";
    document.getElementById("tsa-key-save").addEventListener("click", function() {
      var val = (document.getElementById("tsa-key-input").value || "").trim();
      if (!val) return;
      lsSet("tsa-torn-apikey", val);
      TORN_API_KEY = val;
      if (onSave) onSave();
    });
  }

  // On load: handle PDA key injection, do NOT prompt — onboarding shown inline when needed
  if (!TORN_API_KEY || TORN_API_KEY === "###PDA-APIKEY###") {
    var pdaKey = "###PDA-APIKEY###";
    if (pdaKey.indexOf("PDA-APIKEY") === -1) {
      // Running in PDA — key was injected
      TORN_API_KEY = pdaKey;
      lsSet("tsa-torn-apikey", TORN_API_KEY);
    }
  }
  var roiPlannerActive = false;
  var isDesktop = window.innerWidth > 768 || !/Mobi|Android/i.test(navigator.userAgent);
  var autoRefreshTimer = null;
  var autoRefreshEndTime = null;
  var autoRefreshCountdownInterval = null;
  var lastLoadPrices = {};
  var tsaPinned = (function() { try { return JSON.parse(localStorage.getItem("tsa_pinned") || "[]") || []; } catch(e) { return []; } })();

  function getAutoRefreshInterval() {
    return parseInt(lsGet("tsa-auto-refresh-interval", "0"), 10);
  }

  function scheduleAutoRefresh() {
    if (autoRefreshTimer) { clearTimeout(autoRefreshTimer); autoRefreshTimer = null; }
    if (autoRefreshCountdownInterval) { clearInterval(autoRefreshCountdownInterval); autoRefreshCountdownInterval = null; }
    autoRefreshEndTime = null;
    var mins = getAutoRefreshInterval();
    if (mins > 0) {
      autoRefreshEndTime = Date.now() + mins * 60000;
      autoRefreshCountdownInterval = setInterval(updateCountdownLabel, 1000);
      autoRefreshTimer = setTimeout(function() {
        autoRefreshTimer = null;
        if (autoRefreshCountdownInterval) { clearInterval(autoRefreshCountdownInterval); autoRefreshCountdownInterval = null; }
        autoRefreshEndTime = null;
        loadData();
      }, mins * 60000);
    }
  }
  // ── Price Alerts ──────────────────────────────────────────────────────────
  var ALERTS_KEY = "tsa_price_alerts";

  function loadAlerts() {
    try { return JSON.parse(localStorage.getItem(ALERTS_KEY)) || []; } catch(e) { return []; }
  }

  function saveAlerts(alerts) {
    lsSet(ALERTS_KEY, JSON.stringify(alerts));
  }

  function addAlert(sym, price, dir, repeat) {
    var alerts = loadAlerts();
    // Remove any existing alert for same sym+dir
    alerts = alerts.filter(function(a) { return !(a.sym === sym && a.dir === dir); });
    alerts.push({ sym: sym.toUpperCase(), price: price, dir: dir, repeat: !!repeat });
    saveAlerts(alerts);
  }

  function removeAlert(sym, dir) {
    var alerts = loadAlerts().filter(function(a) { return !(a.sym === sym && a.dir === dir); });
    saveAlerts(alerts);
  }

  function checkAlerts(raw) {
    if (!raw || !raw.length) return;
    var alerts = loadAlerts();
    if (!alerts.length) return;
    var fired = [];
    alerts.forEach(function(a) {
      var entry = raw.find(function(r) { return r.stock === a.sym; });
      if (!entry) return;
      var live = parseFloat(entry.price) || 0;
      if (live <= 0) return;
      var triggered = (a.dir === "above" && live >= a.price) || (a.dir === "below" && live <= a.price);
      if (triggered) fired.push({ sym: a.sym, price: a.price, dir: a.dir, live: live });
    });
    if (!fired.length) return;
    // Remove one-shot fired alerts; keep repeat alerts active
    var firedKeys = fired.map(function(f) { return f.sym + f.dir; });
    saveAlerts(alerts.filter(function(a) { return a.repeat || firedKeys.indexOf(a.sym + a.dir) < 0; }));
    // Notify
    fired.forEach(function(f) {
      var msg = f.sym + " is " + (f.dir === "above" ? "above" : "below") + " $" + f.price.toFixed(2) + " (live: $" + f.live.toFixed(2) + ")";
      if (typeof Notification !== "undefined" && Notification.permission === "granted") {
        new Notification("Torn Stock Alert", { body: msg, icon: "https://www.torn.com/favicon.ico" });
      } else {
        showToast("Price Alert: " + msg, "warn");
      }
    });
  }

  function requestNotificationPermission() {
    if (typeof Notification !== "undefined" && Notification.permission === "default") {
      Notification.requestPermission();
    }
  }
  // ─────────────────────────────────────────────────────────────────────────

  var lastOwnedMap = null;
  var lastRaw = null;
  var lastBestRec = null; // Best ROI recommendation from last data load

  var STOCK_ID_MAP = {
    1:"TSB",  2:"TCI",  3:"SYS",  4:"LAG",  5:"IOU",
    6:"GRN",  7:"THS",  8:"YAZ",  9:"TCT", 10:"CNC",
    11:"MSG", 12:"TMI", 13:"TCP", 14:"IIL", 15:"FHG",
    16:"SYM", 17:"LSC", 18:"PRN", 19:"EWM", 20:"TCM",
    21:"ELT", 22:"HRG", 23:"TGP", 24:"MUN", 25:"WSU",
    26:"IST", 27:"BAG", 28:"EVL", 29:"MCS", 30:"WLT",
    31:"TCC", 32:"ASS", 33:"CBD", 34:"LOS", 35:"PTS"
  };

  var PASSIVE_STOCKS = ["ELT","IIL","IST","LOS","MSG","SYS","TCP","TCM","TCI","TGP","WSU","WLT","YAZ"];

  // Benefit requirement per stock (shares for 1 increment)
  var BENEFIT_REQ = {
    "TSB":3000000, "TCI":1500000, "SYS":3000000, "LAG":750000,  "IOU":3000000,
    "GRN":500000,  "THS":150000,  "YAZ":1000000,  "TCT":100000,  "CNC":7500000,
    "MSG":300000,  "TMI":6000000, "TCP":1000000,  "IIL":1000000, "FHG":2000000,
    "SYM":500000,  "LSC":500000,  "PRN":1000000,  "EWM":1000000, "TCM":1000000,
    "ELT":5000000, "HRG":10000000,"TGP":2500000,  "MUN":5000000, "WSU":1000000,
    "IST":100000,  "BAG":3000000, "EVL":100000,   "MCS":350000,  "WLT":9000000,
    "TCC":7500000, "ASS":1000000, "CBD":350000,   "LOS":7500000, "PTS":10000000
  };

  var STOCKS_LIST = ["ass","bag","cbd","cnc","elt","evl","ewm","fhg","grn","hrg",
    "iil","iou","ist","lag","los","lsc","mcs","msg","mun","prn",
    "pts","sym","sys","tcc","tci","tcm","tcp","tct","tgp","ths",
    "tmi","tsb","wlt","wsu","yaz"];

var STYLES = "\n\n    #tsa-btn {\n\n      position: fixed; bottom: 80px; right: 16px; z-index: 2147483647;\n\n      background: #4a6fa5; color: #ffffff; border: none;\n\n      border-radius: 50px; padding: 10px 18px; font-size: 13px;\n\n      font-family: Arial, sans-serif; cursor: pointer; font-weight: bold;\n\n      box-shadow: 0 2px 8px rgba(0,0,0,0.3);\n\n      -webkit-tap-highlight-color: transparent;\n\n    }\n\n    #tsa-btn:hover { background: #3a5f95; }\n\n    #tsa-overlay {\n\n      position: fixed; bottom: 130px; right: 16px; z-index: 2147483646;\n\n      max-height: 75vh; overflow-y: auto;\n\n      background: #ffffff; border: 1px solid #ddd; border-radius: 12px;\n\n      font-family: Arial, sans-serif; font-size: 12px; color: #222;\n\n      box-shadow: 0 4px 20px rgba(0,0,0,0.15); display: none;\n\n    }\n\n    #tsa-overlay::-webkit-scrollbar { width: 4px; }\n\n    #tsa-overlay::-webkit-scrollbar-thumb { background: #ccc; border-radius: 2px; }\n\n    .tsa-header {\n\n      display: flex; align-items: center; justify-content: space-between;\n\n      padding: 12px 14px; border-bottom: 1px solid #eee;\n\n      position: sticky; top: 0; background: #ffffff; z-index: 1;\n\n    }\n\n    .tsa-header-left { display: flex; align-items: center; gap: 8px; }\n\n    .tsa-title { font-size: 13px; font-weight: bold; color: #4a6fa5; letter-spacing: 0.05em; }\n\n    .tsa-theme-btn {\n\n      font-size: 14px; cursor: pointer; background: none; border: none;\n\n      padding: 2px 4px; line-height: 1; opacity: 0.7;\n\n    }\n\n    .tsa-theme-btn:hover { opacity: 1; }\n\n    .tsa-close { cursor: pointer; color: #777; font-size: 18px; padding: 0 4px; line-height: 1; }\n\n    .tsa-close:hover { color: #333; }\n\n    .tsa-stats {\n\n      display: grid; grid-template-columns: repeat(3, 1fr);\n\n      gap: 8px; padding: 12px 14px; border-bottom: 1px solid #eee;\n\n    }\n\n    .tsa-stat { background: #f7f9fc; border-radius: 8px; padding: 8px; text-align: center; border: 1px solid #e8edf5; }\n\n    .tsa-stat-label { font-size: 10px; color: #666; margin-bottom: 4px; }\n\n    .tsa-stat-value { font-size: 16px; font-weight: bold; color: #222; }\n\n    .tsa-stat-value.green { color: #1a8a45; }\n\n    .tsa-stat-value.red { color: #cc2222; }\n\n    .tsa-section { padding: 10px 14px 6px; }\n\n    .tsa-section-title { font-size: 10px; letter-spacing: 0.12em; color: #777; text-transform: uppercase; margin-bottom: 8px; font-weight: bold; }\n\n    .tsa-row {\n\n      display: flex; align-items: center; justify-content: space-between;\n\n      padding: 8px 10px; border-radius: 8px; margin-bottom: 5px; cursor: pointer;\n\n    }\n\n    .tsa-row.buy { background: #edfaf3; border: 1px solid #a8e6c0; }\n\n    .tsa-row.sell { background: #fff0f0; border: 1px solid #ffb3b3; }\n\n    .tsa-row.hold { background: #f0f4ff; border: 1px solid #c0d0ff; }\n\n    .tsa-row.buy:active { background: #d0f5e3; }\n\n    .tsa-row.sell:active { background: #ffd8d8; }\n\n    .tsa-row.hold:active { background: #dce6ff; }\n\n    .tsa-row-left { display: flex; flex-direction: column; gap: 2px; }\n\n    .tsa-symbol { font-size: 13px; font-weight: bold; }\n\n    .tsa-symbol.buy { color: #1a8a45; }\n\n    .tsa-symbol.sell { color: #cc2222; }\n\n    .tsa-symbol.hold { color: #4a6fa5; }\n\n    .tsa-detail { font-size: 10px; color: #666; }\n\n    .tsa-row-right { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }\n\n    .tsa-score { font-size: 14px; font-weight: bold; }\n\n    .tsa-score.buy { color: #1a8a45; }\n\n    .tsa-score.sell { color: #cc2222; }\n\n    .tsa-score.hold { color: #4a6fa5; }\n\n    .tsa-badge { font-size: 9px; padding: 2px 6px; border-radius: 10px; font-weight: bold; }\n\n    .tsa-badge.benefit { background: #fff3cd; color: #856404; border: 1px solid #ffc107; }\n\n    .tsa-divider { border: none; border-top: 1px solid #eee; margin: 6px 14px; }\n\n    .tsa-footer {\n\n      padding: 10px 14px; display: flex; justify-content: space-between;\n\n      align-items: center; border-top: 1px solid #eee; background: #fafafa;\n\n      border-radius: 0 0 12px 12px;\n\n    }\n\n    .tsa-updated { font-size: 10px; color: #888; }\n\n    .tsa-refresh {\n\n      font-size: 11px; background: #4a6fa5; border: none;\n\n      color: #fff; border-radius: 6px; padding: 5px 12px; cursor: pointer;\n\n      font-family: Arial, sans-serif; font-weight: bold;\n\n    }\n\n    .tsa-refresh:hover { background: #3a5f95; }\n\n    .tsa-loading { padding: 30px; text-align: center; color: #888; font-size: 12px; }\n\n    @keyframes tsa-spin { to { transform: rotate(360deg); } }\n\n    .tsa-spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%; animation: tsa-spin 0.7s linear infinite; vertical-align: middle; margin-right: 6px; opacity: 0.6; }\n\n    .tsa-error { padding: 16px; color: #cc2222; font-size: 11px; text-align: center; }\n\n    /* DARK MODE */\n\n    #tsa-overlay.tsa-dark {\n\n      background: #0f0f1a; border-color: #3a3a6a; color: #c8c8d8;\n\n    }\n\n    #tsa-overlay.tsa-dark::-webkit-scrollbar-thumb { background: #3a3a6a; }\n\n    #tsa-overlay.tsa-dark .tsa-header { background: #0f0f1a; border-color: #2a2a4a; }\n\n    #tsa-overlay.tsa-dark .tsa-stats { border-color: #2a2a4a; }\n\n    #tsa-overlay.tsa-dark .tsa-stat { background: #1a1a2e; border-color: #2a2a4a; }\n\n    #tsa-overlay.tsa-dark .tsa-stat-label { color: #888; }\n\n    #tsa-overlay.tsa-dark .tsa-stat-value { color: #e0e0ff; }\n\n    #tsa-overlay.tsa-dark .tsa-section-title { color: #888; }\n\n    #tsa-overlay.tsa-dark .tsa-detail { color: #888; }\n\n    #tsa-overlay.tsa-dark .tsa-row.buy { background: rgba(76,255,145,0.08); border-color: rgba(76,255,145,0.2); }\n\n    #tsa-overlay.tsa-dark .tsa-row.sell { background: rgba(255,76,106,0.08); border-color: rgba(255,76,106,0.2); }\n\n    #tsa-overlay.tsa-dark .tsa-row.hold { background: rgba(160,160,255,0.05); border-color: rgba(160,160,255,0.1); }\n\n    #tsa-overlay.tsa-dark .tsa-row.buy:active { background: rgba(76,255,145,0.18); }\n\n    #tsa-overlay.tsa-dark .tsa-row.sell:active { background: rgba(255,76,106,0.18); }\n\n    #tsa-overlay.tsa-dark .tsa-row.hold:active { background: rgba(160,160,255,0.14); }\n\n    #tsa-overlay.tsa-dark .tsa-symbol.buy { color: #4cff91; }\n\n    #tsa-overlay.tsa-dark .tsa-symbol.sell { color: #ff4c6a; }\n\n    #tsa-overlay.tsa-dark .tsa-symbol.hold { color: #a0a0ff; }\n\n    #tsa-overlay.tsa-dark .tsa-stat-value.green { color: #4cff91; }\n\n    #tsa-overlay.tsa-dark .tsa-stat-value.red { color: #ff4c6a; }\n\n    #tsa-overlay.tsa-dark .tsa-divider { border-color: #2a2a4a; }\n\n    #tsa-overlay.tsa-dark .tsa-footer { background: #0f0f1a; border-color: #2a2a4a; }\n\n    #tsa-overlay.tsa-dark .tsa-updated { color: #666; }\n\n    #tsa-overlay.tsa-dark .tsa-loading { color: #666; }\n\n    #tsa-overlay.tsa-dark .tsa-close { color: #888; }\n\n    #tsa-overlay.tsa-dark .tsa-close:hover { color: #aaa; }\n\n    #tsa-overlay.tsa-dark .tsa-theme-btn { color: #c8c8d8; opacity: 1; }\n\n    #tsa-overlay.tsa-dark .tsa-theme-btn:hover { color: #ffffff; }\n\n    #tsa-overlay.tsa-dark .tsa-title { color: #7a9fd4; }\n\n \\n"

  // ============================================================
  // ROI PLANNER
  // ============================================================

  var ROI_TABLE = [
    {sym:"SYM",tier:"T1",cost:353970000,payout:4153424,freq:7,type:"variable",item:370},
    {sym:"FHG",tier:"T1",cost:1735720000,payout:12390647,freq:7,type:"variable",item:367},
    {sym:"TCT",tier:"T1",cost:32121000,payout:1000000,freq:31,type:"fixed",item:0},
    {sym:"PRN",tier:"T1",cost:614190000,payout:4019972,freq:7,type:"variable",item:366},
    {sym:"SYM",tier:"T2",cost:707940000,payout:4153424,freq:7,type:"variable",item:370},
    {sym:"GRN",tier:"T1",cost:154005000,payout:4000000,freq:31,type:"fixed",item:0},
    {sym:"IOU",tier:"T1",cost:537810000,payout:12000000,freq:31,type:"fixed",item:0},
    {sym:"THS",tier:"T1",cost:58455000,payout:272431,freq:7,type:"variable",item:365},
    {sym:"MUN",tier:"T1",cost:2764800000,payout:12705756,freq:7,type:"variable",item:818},
    {sym:"PTS",tier:"T1",cost:770100000,payout:3000000,freq:7,type:"volatile",item:0},
    {sym:"TMI",tier:"T1",cost:1395240000,payout:25000000,freq:31,type:"fixed",item:0},
    {sym:"HRG",tier:"T1",cost:2716600000,payout:45456058,freq:31,type:"random",item:0},
    {sym:"EWM",tier:"T1",cost:287840000,payout:1080642,freq:7,type:"variable",item:364},
    {sym:"FHG",tier:"T2",cost:3471440000,payout:12390647,freq:7,type:"variable",item:367},
    {sym:"TCT",tier:"T2",cost:64242000,payout:1000000,freq:31,type:"fixed",item:0},
    {sym:"PRN",tier:"T2",cost:1228380000,payout:4019972,freq:7,type:"variable",item:366},
    {sym:"TSB",tier:"T1",cost:3538500000,payout:50000000,freq:31,type:"fixed",item:0},
    {sym:"LSC",tier:"T1",cost:272735000,payout:861423,freq:7,type:"variable",item:369},
    {sym:"SYM",tier:"T3",cost:1415880000,payout:4153424,freq:7,type:"variable",item:370},
    {sym:"GRN",tier:"T2",cost:308010000,payout:4000000,freq:31,type:"fixed",item:0},
    {sym:"CNC",tier:"T1",cost:6570750000,payout:80000000,freq:31,type:"fixed",item:0},
    {sym:"IOU",tier:"T2",cost:1075620000,payout:12000000,freq:31,type:"fixed",item:0},
    {sym:"ASS",tier:"T1",cost:356470000,payout:894596,freq:7,type:"variable",item:817},
    {sym:"THS",tier:"T2",cost:116910000,payout:272431,freq:7,type:"variable",item:365},
    {sym:"MUN",tier:"T2",cost:5529600000,payout:12705756,freq:7,type:"variable",item:818},
    {sym:"PTS",tier:"T2",cost:1540200000,payout:3000000,freq:7,type:"volatile",item:0},
    {sym:"TMI",tier:"T2",cost:2790480000,payout:25000000,freq:31,type:"fixed",item:0},
    {sym:"HRG",tier:"T2",cost:5433200000,payout:45456058,freq:31,type:"random",item:0},
    {sym:"EWM",tier:"T2",cost:575680000,payout:1080642,freq:7,type:"variable",item:364},
    {sym:"FHG",tier:"T3",cost:6942880000,payout:12390647,freq:7,type:"variable",item:367},
    {sym:"TCT",tier:"T3",cost:128484000,payout:1000000,freq:31,type:"fixed",item:0},
    {sym:"TCC",tier:"T1",cost:3850875000,payout:29526634,freq:31,type:"variable",item:0},
    {sym:"PRN",tier:"T3",cost:2456760000,payout:4019972,freq:7,type:"variable",item:366},
    {sym:"TSB",tier:"T2",cost:7077000000,payout:50000000,freq:31,type:"fixed",item:0},
    {sym:"LSC",tier:"T2",cost:545470000,payout:861423,freq:7,type:"variable",item:369},
    {sym:"SYM",tier:"T4",cost:2831760000,payout:4153424,freq:7,type:"variable",item:370},
    {sym:"GRN",tier:"T3",cost:616020000,payout:4000000,freq:31,type:"fixed",item:0},
    {sym:"CNC",tier:"T2",cost:13141500000,payout:80000000,freq:31,type:"fixed",item:0},
    {sym:"IOU",tier:"T3",cost:2151240000,payout:12000000,freq:31,type:"fixed",item:0},
    {sym:"ASS",tier:"T2",cost:712940000,payout:894596,freq:7,type:"variable",item:817},
    {sym:"THS",tier:"T3",cost:233820000,payout:272431,freq:7,type:"variable",item:365},
    {sym:"MUN",tier:"T3",cost:11059200000,payout:12705756,freq:7,type:"variable",item:818},
    {sym:"PTS",tier:"T3",cost:3080400000,payout:3000000,freq:7,type:"volatile",item:0},
    {sym:"TMI",tier:"T3",cost:5580960000,payout:25000000,freq:31,type:"fixed",item:0},
    {sym:"HRG",tier:"T3",cost:10866400000,payout:45456058,freq:31,type:"random",item:0},
    {sym:"EWM",tier:"T3",cost:1151360000,payout:1080642,freq:7,type:"variable",item:364},
    {sym:"FHG",tier:"T4",cost:13885760000,payout:12390647,freq:7,type:"variable",item:367},
    {sym:"TCT",tier:"T4",cost:256968000,payout:1000000,freq:31,type:"fixed",item:0},
    {sym:"TCC",tier:"T2",cost:7701750000,payout:29526634,freq:31,type:"variable",item:0},
    {sym:"PRN",tier:"T4",cost:4913520000,payout:4019972,freq:7,type:"variable",item:366},
    {sym:"TSB",tier:"T3",cost:14154000000,payout:50000000,freq:31,type:"fixed",item:0},
    {sym:"LSC",tier:"T3",cost:1090940000,payout:861423,freq:7,type:"variable",item:369},
    {sym:"SYM",tier:"T5",cost:5663520000,payout:4153424,freq:7,type:"variable",item:370},
    {sym:"GRN",tier:"T4",cost:1232040000,payout:4000000,freq:31,type:"fixed",item:0},
    {sym:"CNC",tier:"T3",cost:26283000000,payout:80000000,freq:31,type:"fixed",item:0},
    {sym:"IOU",tier:"T4",cost:4302480000,payout:12000000,freq:31,type:"fixed",item:0},
    {sym:"ASS",tier:"T3",cost:1425880000,payout:894596,freq:7,type:"variable",item:817},
    {sym:"THS",tier:"T4",cost:467640000,payout:272431,freq:7,type:"variable",item:365},
    {sym:"LAG",tier:"T1",cost:353557500,payout:203827,freq:7,type:"variable",item:368},
    {sym:"MUN",tier:"T4",cost:22118400000,payout:12705756,freq:7,type:"variable",item:818},
    {sym:"PTS",tier:"T4",cost:6160800000,payout:3000000,freq:7,type:"volatile",item:0},
    {sym:"TMI",tier:"T4",cost:11161920000,payout:25000000,freq:31,type:"fixed",item:0},
    {sym:"HRG",tier:"T4",cost:21732800000,payout:45456058,freq:31,type:"random",item:0},
    {sym:"EWM",tier:"T4",cost:2302720000,payout:1080642,freq:7,type:"variable",item:364},
    {sym:"FHG",tier:"T5",cost:27771520000,payout:12390647,freq:7,type:"variable",item:367},
    {sym:"TCT",tier:"T5",cost:513936000,payout:1000000,freq:31,type:"fixed",item:0},
    {sym:"TCC",tier:"T3",cost:15403500000,payout:29526634,freq:31,type:"variable",item:0},
    {sym:"PRN",tier:"T5",cost:9827040000,payout:4019972,freq:7,type:"variable",item:366},
    {sym:"TSB",tier:"T4",cost:28308000000,payout:50000000,freq:31,type:"fixed",item:0},
    {sym:"LSC",tier:"T4",cost:2181880000,payout:861423,freq:7,type:"variable",item:369},
    {sym:"SYM",tier:"T6",cost:11327040000,payout:4153424,freq:7,type:"variable",item:370},
    {sym:"GRN",tier:"T5",cost:2464080000,payout:4000000,freq:31,type:"fixed",item:0},
    {sym:"CNC",tier:"T4",cost:52566000000,payout:80000000,freq:31,type:"fixed",item:0},
    {sym:"IOU",tier:"T5",cost:8604960000,payout:12000000,freq:31,type:"fixed",item:0},
    {sym:"ASS",tier:"T4",cost:2851760000,payout:894596,freq:7,type:"variable",item:817},
    {sym:"THS",tier:"T5",cost:935280000,payout:272431,freq:7,type:"variable",item:365},
    {sym:"LAG",tier:"T2",cost:707115000,payout:203827,freq:7,type:"variable",item:368},
    {sym:"MUN",tier:"T5",cost:44236800000,payout:12705756,freq:7,type:"variable",item:818},
    {sym:"PTS",tier:"T5",cost:12321600000,payout:3000000,freq:7,type:"volatile",item:0},
    {sym:"TMI",tier:"T5",cost:22323840000,payout:25000000,freq:31,type:"fixed",item:0},
    {sym:"HRG",tier:"T5",cost:43465600000,payout:45456058,freq:31,type:"random",item:0},
    {sym:"EWM",tier:"T5",cost:4605440000,payout:1080642,freq:7,type:"variable",item:364},
    {sym:"FHG",tier:"T6",cost:55543040000,payout:12390647,freq:7,type:"variable",item:367},
    {sym:"TCT",tier:"T6",cost:1027872000,payout:1000000,freq:31,type:"fixed",item:0},
    {sym:"TCC",tier:"T4",cost:30807000000,payout:29526634,freq:31,type:"variable",item:0},
    {sym:"PRN",tier:"T6",cost:19654080000,payout:4019972,freq:7,type:"variable",item:366},
    {sym:"TSB",tier:"T5",cost:56616000000,payout:50000000,freq:31,type:"fixed",item:0},
    {sym:"LSC",tier:"T5",cost:4363760000,payout:861423,freq:7,type:"variable",item:369}
  ];

  // Lookup map for O(1) ROI_TABLE access: key = "SYM|Tn"
  var ROI_MAP = (function() {
    var m = {};
    ROI_TABLE.forEach(function(e) { m[e.sym + "|" + e.tier] = e; });
    return m;
  })();

  // Item IDs with sellable market value
  var ITEM_IDS = [364, 365, 366, 367, 368, 369, 370, 817, 818];
  // PTS gives 100 points = $3M fixed
  var PTS_VALUE = 3000000;

  var roiSkipped = (function() { try { return JSON.parse(localStorage.getItem("tsa_roi_skipped") || "[]") || []; } catch(e) { return []; } })();
  var itemPrices = {}; // cache: itemId -> price

  function fmRoi(n) {
    if (n >= 1e9) return "$" + (n/1e9).toFixed(2) + "B";
    if (n >= 1e6) return "$" + (n/1e6).toFixed(2) + "M";
    if (n >= 1e3) return "$" + (n/1e3).toFixed(0) + "K";
    return "$" + n.toFixed(0);
  }

  function fetchItemPrice(itemId, cb) {
    if (itemPrices[itemId] !== undefined) { cb(itemPrices[itemId]); return; }
    var url = "https://api.torn.com/market/" + itemId + "?selections=itemmarket&key=" + getTornKey();
    GM_xmlhttpRequest({
      method: "GET", url: url,
      onload: function(r) {
        try {
          var d = JSON.parse(r.responseText);
          var listings = d.itemmarket && d.itemmarket.listings ? d.itemmarket.listings : [];
          if (listings.length > 0) {
            // Use lowest price
            var lowest = listings.reduce(function(a,b){ return a.price < b.price ? a : b; });
            itemPrices[itemId] = lowest.price;
            cb(lowest.price);
          } else { itemPrices[itemId] = 0; cb(0); }
        } catch(e) { itemPrices[itemId] = 0; cb(0); }
      },
      onerror: function() { itemPrices[itemId] = 0; cb(0); }
    });
  }

  function fetchAllItemPrices(cb) {
    var remaining = ITEM_IDS.length;
    var done = false;
    function finish() { if (!done) { done = true; cb(); } }
    setTimeout(finish, 10000); // failsafe: call cb after 10s even if a request never returns
    ITEM_IDS.forEach(function(id) {
      fetchItemPrice(id, function() {
        remaining--;
        if (remaining === 0) finish();
      });
    });
  }

  function getItemValue(entry) {
    if (entry.sym === "PTS") return PTS_VALUE;
    if (entry.item && itemPrices[entry.item]) return itemPrices[entry.item];
    return 0;
  }

  // Calculate Bollinger Bands using all stored price history
  // Returns { upper, middle, lower, pctB } or null if insufficient data
  // pctB = position within bands: 0 = at lower, 1 = at upper, <0 = below lower
  function calcBollingerBands(sym, history, livePrice) {
    var entries = history ? history[sym.toUpperCase()] : null;
    if (!entries || entries.length < 20) return null;
    var prices = entries.map(function(e) { return e.price; });
    // Use last 20 prices for SMA and stddev
    var period = 20;
    var slice = prices.slice(-period);
    var sma = slice.reduce(function(a, b) { return a + b; }, 0) / period;
    var variance = slice.reduce(function(sum, p) { return sum + Math.pow(p - sma, 2); }, 0) / period;
    var stddev = Math.sqrt(variance);
    var upper = sma + 2 * stddev;
    var lower = sma - 2 * stddev;
    var range = upper - lower;
    if (range === 0) return null; // no volatility — bands are meaningless
    var pctB = (livePrice - lower) / range;
    return { upper: upper, middle: sma, lower: lower, pctB: pctB };
  }
  // Returns { macd, signal, histogram, crossover } or null if insufficient data
  function calcMACD(sym, history) {
    var entries = history ? history[sym.toUpperCase()] : null;
    if (!entries || entries.length < 35) return null;
    var sorted = entries.slice().sort(function(a, b) { return a.ts - b.ts; });
    var prices = sorted.map(function(e) { return e.price; });
    if (prices.length < 35) return null;

    function calcEMA(data, period) {
      var k = 2 / (period + 1);
      var ema = [data[0]];
      for (var i = 1; i < data.length; i++) {
        ema.push(data[i] * k + ema[i-1] * (1 - k));
      }
      return ema;
    }

    var ema12 = calcEMA(prices, 12);
    var ema26 = calcEMA(prices, 26);
    var macdLine = ema12.map(function(v, i) { return v - ema26[i]; });
    var signalLine = calcEMA(macdLine, 9); // signal line computed over full macdLine for aligned indices
    var lastMacd = macdLine[macdLine.length - 1];
    var lastSignal = signalLine[signalLine.length - 1];
    var prevMacd = macdLine[macdLine.length - 2];
    var prevSignal = signalLine[signalLine.length - 2];

    // Bullish crossover: MACD crossed above signal line
    var crossover = prevMacd < prevSignal && lastMacd >= lastSignal;

    return {
      macd: lastMacd,
      signal: lastSignal,
      histogram: lastMacd - lastSignal,
      crossover: crossover
    };
  }

  // Low-level RSI from a price array (min 15 prices, 14-period Wilder)
  function rsiFromPrices(prices) {
    if (!prices || prices.length < 15) return null;
    var changes = [];
    for (var i = 1; i < prices.length; i++) changes.push(prices[i] - prices[i - 1]);
    var period = 14;
    var gains = 0, losses = 0;
    for (var j = 0; j < period; j++) {
      if (changes[j] >= 0) gains += changes[j];
      else losses += Math.abs(changes[j]);
    }
    var avgGain = gains / period, avgLoss = losses / period;
    for (var k = period; k < changes.length; k++) {
      var g = changes[k] >= 0 ? changes[k] : 0;
      var l = changes[k] < 0 ? Math.abs(changes[k]) : 0;
      avgGain = (avgGain * 13 + g) / 14;
      avgLoss = (avgLoss * 13 + l) / 14;
    }
    if (avgGain === 0 && avgLoss === 0) return null;
    if (avgLoss === 0) return 100;
    return 100 - (100 / (1 + avgGain / avgLoss));
  }

  // Calculate RSI using all stored price history (kept for backward compat)
  function calcRSI(sym, history) {
    var entries = history ? history[sym.toUpperCase()] : null;
    if (!entries || entries.length < 15) return null;
    return rsiFromPrices(entries.map(function(e) { return e.price; }));
  }

  // Torn-specific RSI context: returns current RSI + its percentile within this
  // stock's own historical RSI range. Uses 28-price windows (28h of hourly data)
  // stepped every 4 prices through history — auto-calibrates to each stock's
  // normal RSI behaviour instead of relying on generic 30/70 real-market thresholds.
  function calcRSIContext(sym, history) {
    var entries = history ? history[sym.toUpperCase()] : null;
    if (!entries || entries.length < 28) return null;
    var sorted = entries.slice().sort(function(a, b) { return a.ts - b.ts; });
    var prices = sorted.map(function(e) { return e.price; });

    var current = rsiFromPrices(prices.slice(-28));
    if (current === null) return null;

    // Build historical RSI sample: one value per 4-price step
    var historicalRSI = [];
    for (var i = 28; i <= prices.length; i += 4) {
      var r = rsiFromPrices(prices.slice(i - 28, i));
      if (r !== null) historicalRSI.push(r);
    }

    if (historicalRSI.length < 5) {
      // Not enough history for percentile — return RSI only
      return { rsi: current, percentile: null };
    }

    historicalRSI.sort(function(a, b) { return a - b; });
    var below = historicalRSI.filter(function(r) { return r <= current; }).length;
    return { rsi: current, percentile: (below / historicalRSI.length) * 100 };
  }
  // Bollinger Band width context for Torn stocks.
  // BB width = (2 * stddev / SMA) * 100 over a 20-price window.
  // Returns the current width's percentile within the stock's own historical
  // BB widths so the caller knows if the stock is in a squeeze (narrow band,
  // low volatility — breakout pending) or an active/expanded band.
  // Percentile is always relative to this stock's own history, not a fixed scale.
  function calcBBWidth(sym, history) {
    var entries = history ? history[sym.toUpperCase()] : null;
    if (!entries || entries.length < 20) return null;
    var sorted = entries.slice().sort(function(a, b) { return a.ts - b.ts; });
    var prices = sorted.map(function(e) { return e.price; });

    function bbWidthFromSlice(slice) {
      if (slice.length < 20) return null;
      var n = 20;
      var window = slice.slice(-n);
      var sma = window.reduce(function(s, v) { return s + v; }, 0) / n;
      if (sma <= 0) return null;
      var variance = window.reduce(function(s, v) { return s + (v - sma) * (v - sma); }, 0) / n;
      var stddev = Math.sqrt(variance);
      return (2 * stddev / sma) * 100;
    }

    var current = bbWidthFromSlice(prices);
    if (current === null) return null;

    // Historical sample: one BB width every 4 prices
    var historicalWidths = [];
    for (var i = 20; i <= prices.length; i += 4) {
      var w = bbWidthFromSlice(prices.slice(0, i));
      if (w !== null) historicalWidths.push(w);
    }

    if (historicalWidths.length < 5) return { width: current, percentile: null };

    historicalWidths.sort(function(a, b) { return a - b; });
    var below = historicalWidths.filter(function(w) { return w <= current; }).length;
    var percentile = (below / historicalWidths.length) * 100;

    var label;
    if      (percentile <= 15) label = "Squeeze";
    else if (percentile <= 35) label = "Low volatility";
    else if (percentile <= 65) label = "Normal";
    else if (percentile <= 85) label = "Active";
    else                       label = "High volatility";

    return { width: current, percentile: percentile, label: label };
  }

  // Uses BENEFIT_REQ (fixed share counts) and dividend.increment from API
  // Next tier total shares = BENEFIT_REQ × (2^nextIncrement - 1)
  // Next tier cost = sharesNeeded × live_price
  function calcNextTier(sym, ownedMap, raw) {
    var req = BENEFIT_REQ[sym];
    if (!req) return null;
    var liveEntry = raw ? raw.find(function(x) { return x.stock === sym; }) : null;
    var livePrice = liveEntry ? (parseFloat(liveEntry.price) || 0) : 0;
    if (livePrice <= 0) return null;

    var o = ownedMap[sym];
    var currentIncrement = o ? (o.dividend_increment || 0) : 0;
    var currentShares = o ? (o.shares || 0) : 0;

    var nextIncrement = currentIncrement + 1;
    var totalSharesNeeded = (Math.pow(2, nextIncrement) - 1) * req;
    var sharesNeeded = Math.max(0, totalSharesNeeded - currentShares);
    var cost = sharesNeeded * livePrice;

    return {
      sym: sym,
      currentIncrement: currentIncrement,
      nextIncrement: nextIncrement,
      totalSharesNeeded: totalSharesNeeded,
      sharesNeeded: sharesNeeded,
      cost: cost,
      livePrice: livePrice
    };
  }
  // ownedMap: from buildOwnedMap, raw: from tornsy for live prices
  function calcWeeklyIncome(ownedMap, raw, extraEntry) {
    var weeklyTotal = 0;
    // Iterate over all owned stocks with benefit shares
    Object.keys(ownedMap).forEach(function(sym) {
      var o = ownedMap[sym];
      if (!o.has_dividend || o.benefit_shares <= 0) return;
      var increments = o.dividend_increment || 0;
      if (increments <= 0) return;
      // Find matching ROI table entries
      ROI_TABLE.forEach(function(entry) {
        if (entry.sym !== sym) return;
        var tierNum = parseInt(entry.tier.replace("T",""), 10);
        if (tierNum !== increments) return;
        // Payout per 7 days
        var payoutPerDay = entry.payout / entry.freq;
        var itemVal = getItemValue(entry);
        var itemPerDay = itemVal / entry.freq;
        weeklyTotal += (payoutPerDay + itemPerDay) * 7;
      });
    });
    // Add extra entry (bridgebuilder)
    if (extraEntry) {
      var itemVal = getItemValue(extraEntry);
      var payoutPerDay = extraEntry.payout / extraEntry.freq;
      var itemPerDay = itemVal / extraEntry.freq;
      weeklyTotal += (payoutPerDay + itemPerDay) * 7;
    }
    return weeklyTotal;
  }

  // Calculate days until target is affordable with given weekly income + capital
  function daysToAfford(target, capital, weeklyIncome) {
    if (capital >= target) return 0;
    if (weeklyIncome <= 0) return Infinity;
    var needed = target - capital;
    var dailyIncome = weeklyIncome / 7;
    return Math.ceil(needed / dailyIncome);
  }

  function renderROIPlanner(ownedMap, raw, cashBalance, armoryFunds) {
    if (!armoryFunds) armoryFunds = 0;
    // Calculate swing capital
    var swingCapital = 0;
    var swingDetails = [];
    Object.keys(ownedMap).forEach(function(sym) {
      var o = ownedMap[sym];
      if (o.swing_shares <= 0) return;
      var liveEntry = raw ? raw.find(function(x){ return x.stock === sym; }) : null;
      if (!liveEntry) return;
      var livePrice = parseFloat(liveEntry.price) || 0;
      var val = livePrice * o.swing_shares;
      swingCapital += val;
      if (val > 0) swingDetails.push({sym: sym, val: val});
    });

    var totalCapital = cashBalance + swingCapital + armoryFunds;

    // Find owned benefit blocks from the ROI table
    var ownedEntries = ROI_TABLE.filter(function(entry) {
      var o = ownedMap[entry.sym];
      if (!o || !o.has_dividend || o.benefit_shares <= 0) return false;
      var tierNum = parseInt(entry.tier.replace("T",""), 10);
      // Use Torn's own increment field
      return tierNum <= o.dividend_increment;
    });

    // Weekly income from current benefit blocks
    var weeklyIncome = calcWeeklyIncome(ownedMap, raw, null);

    // Find next 3 recommended purchases using dynamic tier costs
    var ownedKeys = ownedEntries.map(function(e){ return e.sym + e.tier; });

    // Build dynamic next tier list for all 35 stocks with benefit data
    var benefitSyms = Object.keys(BENEFIT_REQ);
    var dynamicNextTiers = [];
    benefitSyms.forEach(function(sym) {
      var tierInfo = calcNextTier(sym, ownedMap, raw);
      if (!tierInfo || tierInfo.cost <= 0) return;
      // Find payout data from ROI_MAP for this next increment
      var payoutEntry = ROI_MAP[sym + "|T" + tierInfo.nextIncrement] || null;
      // If no ROI_MAP entry — passive stock, skip (no sellable payout)
      dynamicNextTiers.push({
        sym: sym,
        tierInfo: tierInfo,
        payoutEntry: payoutEntry,
        cost: tierInfo.cost,
        roi: payoutEntry ? (payoutEntry.payout / tierInfo.cost * (365 / payoutEntry.freq) * 100) : 0
      });
    });

    // Sort by ROI descending, filter out skipped
    dynamicNextTiers = dynamicNextTiers.filter(function(e) {
      var key = e.sym + "T" + e.tierInfo.nextIncrement;
      return roiSkipped.indexOf(key) < 0;
    });
    dynamicNextTiers.sort(function(a, b) { return b.roi - a.roi; });

    // Target = best ROI entry regardless of affordability
    var target = dynamicNextTiers[0] || null;
    var nextEntries = target ? [target] : [];
    var nextEntry = target;

    // Bridgebuilder chain: buy multiple bridges you can afford (keep them, don't sell),
    // each one adds dividend income that accelerates reaching Next Move.
    var bridgeChain = [];
    var daysWait = target ? daysToAfford(target.cost, totalCapital, weeklyIncome) : 0;
    var daysBaseline = daysWait;

    if (target) {
      var chainCap    = totalCapital;
      var chainIncome = weeklyIncome;

      // Candidates: not the target itself, not skipped, must have dividend income
      var allBridgeCandidates = dynamicNextTiers.filter(function(e) {
        var key = e.sym + "T" + e.tierInfo.nextIncrement;
        if (roiSkipped.indexOf(key) >= 0) return false;
        if (e.sym === target.sym && e.tierInfo.nextIncrement === target.tierInfo.nextIncrement) return false;
        if (!e.payoutEntry) return false;
        var itemVal = getItemValue(e.payoutEntry);
        return ((e.payoutEntry.payout + itemVal) / e.payoutEntry.freq * 7) > 0;
      });

      allBridgeCandidates.forEach(function(e) {
        var itemVal     = getItemValue(e.payoutEntry);
        var extraIncome = (e.payoutEntry.payout + itemVal) / e.payoutEntry.freq * 7;

        if (e.cost <= chainCap) {
          // Affordable now — buy it, reduce capital, increase income stream
          chainCap    -= e.cost;
          chainIncome += extraIncome;
          bridgeChain.push({
            sym: e.sym, tier: "T" + e.tierInfo.nextIncrement,
            cost: e.cost, extraIncome: extraIncome, roi: e.roi,
            status: "now", daysUntil: 0
          });
        }
        // "Later" bridges added separately below, sorted by soonest
      });

      // After buying all "now" bridges, find the next 2 soonest affordable ones
      // that actually save days vs just waiting — these are the ones to save up for
      var laterCandidates = [];
      allBridgeCandidates.forEach(function(e) {
        if (bridgeChain.some(function(b) { return b.sym === e.sym && b.tier === ("T" + e.tierInfo.nextIncrement); })) return; // already handled as "now"
        var itemVal     = getItemValue(e.payoutEntry);
        var extraIncome = (e.payoutEntry.payout + itemVal) / e.payoutEntry.freq * 7;
        var daysUntil   = daysToAfford(e.cost, chainCap, chainIncome);
        if (daysUntil === Infinity || daysUntil > 365) return;
        // Simulate buying this bridge after daysUntil: does it save days to target?
        var simCap    = chainCap + (chainIncome / 7 * daysUntil) - e.cost;
        var simIncome = chainIncome + extraIncome;
        var daysAfter = daysUntil + daysToAfford(target.cost, simCap, simIncome);
        var saved     = daysBaseline - daysAfter;
        if (saved <= 0) return; // doesn't help
        laterCandidates.push({
          sym: e.sym, tier: "T" + e.tierInfo.nextIncrement,
          cost: e.cost, extraIncome: extraIncome, roi: e.roi,
          status: "later", daysUntil: daysUntil, daysSaved: saved
        });
      });
      // Sort by soonest affordable, keep top 2
      laterCandidates.sort(function(a, b) { return a.daysUntil - b.daysUntil; });
      laterCandidates.slice(0, 2).forEach(function(b) { bridgeChain.push(b); });

      // Recompute goal with all "now" bridges applied (capital reduced, income boosted)
      var daysWithBridges = daysToAfford(target.cost, chainCap, chainIncome);

      // Sell candidates: owned benefit blocks with lower ROI than target
      // ROI of target
      var targetRoi = target.roi || 0;
      var sellCandidates = [];
      ownedEntries.forEach(function(e) {
        var itemVal = getItemValue(e);
        var weekly = e.payout ? (e.payout + itemVal) / (e.freq || 7) * 7 : 0;
        var liveEntry = raw ? raw.find(function(x) { return x.stock === e.sym; }) : null;
        var livePrice = liveEntry ? (parseFloat(liveEntry.price) || 0) : 0;
        var o = ownedMap[e.sym];
        var req = BENEFIT_REQ[e.sym] || 0;
        // Use this entry's specific tier number, not the overall increment
        var entryTierNum = parseInt(e.tier.replace("T",""), 10) || 0;
        // Shares belonging to THIS tier only: (2^n - 2^(n-1)) * BENEFIT_REQ
        var tierShares = entryTierNum > 0 ? (Math.pow(2, entryTierNum) - Math.pow(2, entryTierNum - 1)) * req : 0;
        var saleValue = tierShares * livePrice * 0.999;
        // Live cost of this tier's shares
        var liveTierCost = tierShares * livePrice;
        var entryRoi = liveTierCost > 0 && e.payout ? (e.payout / liveTierCost * (365 / (e.freq || 7)) * 100) : 0;
        if (entryRoi > 0 && entryRoi < targetRoi && saleValue > 0) {
          sellCandidates.push({
            sym: e.sym,
            tier: e.tier,
            roi: entryRoi,
            saleValue: saleValue,
            weekly: weekly
          });
        }
      });
      sellCandidates.sort(function(a, b) { return a.roi - b.roi; }); // lowest ROI first
    }

    // Build HTML
    var isDark = document.getElementById("tsa-overlay").classList.contains("tsa-dark");
    var c = isDark ? {
      bg:"#0f0f1a", border:"#2a2a4a", bg2:"#0d0d18", bg3:"#0c0c16",
      text:"#c8c8d8", muted:"#7a7a9a", mono:"JetBrains Mono,monospace",
      blue:"#7a9fd4", green:"#4cff91", red:"#cc4444", owned_bg:"rgba(40,180,100,0.07)",
      owned_border:"rgba(76,255,145,0.4)", next_bg:"rgba(74,111,165,0.1)",
      next_border:"rgba(122,159,212,0.5)", skip_bg:"rgba(180,40,40,0.06)",
      skip_border:"rgba(255,80,80,0.3)", neutral:"#7a7a9a", row_border:"#13131f",
      divider:"#1a1a2e", tag_bg:"rgba(120,140,200,0.08)", tag_border:"rgba(120,140,200,0.15)", tag_text:"#8898bb",
      bridge_bg:"rgba(90,180,80,0.09)", bridge_border:"rgba(76,255,145,0.45)", bridge:"#5ab450"
    } : {
      bg:"#ffffff", border:"#ddd", bg2:"#f7f9fc", bg3:"#f0f4ff",
      text:"#222", muted:"#666", mono:"Arial,sans-serif",
      blue:"#4a6fa5", green:"#1a8a45", red:"#cc2222", owned_bg:"#edfaf3",
      owned_border:"#a8e6c0", next_bg:"#f0f4ff", next_border:"#c0d0ff",
      skip_bg:"#fff0f0", skip_border:"#ffb3b3", neutral:"#aaa", row_border:"#eee",
      divider:"#eee", tag_bg:"#f0f4ff", tag_border:"#c0d0ff", tag_text:"#4a6fa5",
      bridge_bg:"#edfff0", bridge_border:"#80c880", bridge:"#1a7a35"
    };

    var s = 'font-family:' + c.mono + ';';

    // Capital bar
    var swingTagsHtml = swingDetails.slice(0,4).map(function(d){
      return '<span style="font-size:9px;padding:2px 6px;border-radius:8px;background:' + c.tag_bg + ';border:1px solid ' + c.tag_border + ';color:' + c.tag_text + ';' + s + 'margin-right:4px">' + d.sym + ' ' + fmRoi(d.val) + '</span>';
    }).join('');
    if (armoryFunds > 0) {
      swingTagsHtml = '<span style="font-size:9px;padding:2px 6px;border-radius:8px;background:' + c.tag_bg + ';border:1px solid ' + c.tag_border + ';color:' + c.tag_text + ';' + s + 'margin-right:4px">Armory ' + fmRoi(armoryFunds) + '</span>' + swingTagsHtml;
    }
    if (cashBalance > 0) {
      swingTagsHtml = '<span style="font-size:9px;padding:2px 6px;border-radius:8px;background:' + c.tag_bg + ';border:1px solid ' + c.tag_border + ';color:' + c.tag_text + ';' + s + 'margin-right:4px">Cash ' + fmRoi(cashBalance) + '</span>' + swingTagsHtml;
    }

    var html = '<div style="padding:8px 14px;border-bottom:1px solid ' + c.divider + ';background:' + c.bg2 + '">' +
      '<div style="font-size:9px;letter-spacing:0.1em;color:' + c.muted + ';text-transform:uppercase;margin-bottom:3px">Available capital</div>' +
      '<div style="' + s + 'font-size:14px;font-weight:700;color:' + c.text + '">' + fmRoi(totalCapital) + '</div>' +
      '<div style="margin-top:4px;display:flex;flex-wrap:wrap;gap:4px">' + swingTagsHtml + '</div>' +
      '</div>';

    // Next move section
    if (target) {
      var nmRow = function(label, val, color) {
        return '<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:3px">' +
          '<span style="font-size:9px;color:' + c.muted + ';text-transform:uppercase;letter-spacing:0.08em;' + s + '">' + label + '</span>' +
          '<span style="font-size:10px;color:' + (color||c.text) + ';' + s + ';text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:65%">' + val + '</span>' +
          '</div>';
      };

      html += '<div style="padding:10px 14px;border-bottom:2px solid ' + c.divider + ';background:' + c.bg3 + '">';
      html += '<div style="font-size:9px;letter-spacing:0.12em;text-transform:uppercase;color:' + c.blue + ';' + s + ';font-weight:700;margin-bottom:8px">💡 Next move</div>';

      // Target
      var shortBy = Math.max(0, target.cost - totalCapital);
      var targetTier = target.tier || ("T" + target.tierInfo.nextIncrement);
      html += nmRow("Target", target.sym + " " + targetTier + " · " + (target.roi || 0).toFixed(2) + "% ROI", c.blue);
      html += nmRow("Cost", fmRoi(target.cost) + (shortBy > 0 ? " · short " + fmRoi(shortBy) : " ✓"), shortBy > 0 ? c.red : c.green);
      html += nmRow("Available", fmRoi(totalCapital), c.text);

      // Bridgebuilder chain — buy and hold, each block adds dividend income
      html += '<div style="border-top:1px solid ' + c.divider + ';margin:6px 0"></div>';
      html += '<div style="font-size:9px;color:#5a7a4a;letter-spacing:0.08em;' + s + ';margin-bottom:5px">🔗 Bridgebuilder</div>';

      var hasSell = sellCandidates && sellCandidates.length > 0;

      if (bridgeChain.length > 0) {
        bridgeChain.forEach(function(b) {
          var statusColor = b.status === "now" ? c.green : c.muted;
          var statusLabel = b.status === "now"
            ? "✓ Buy now"
            : "in ~" + b.daysUntil + "d · saves " + b.daysSaved + "d";
          var roiStr = b.roi > 0 ? " · " + b.roi.toFixed(1) + "%" : "";
          html += nmRow(
            "🔗 " + b.sym + " " + b.tier + roiStr,
            fmRoi(b.cost) + " · +" + fmRoi(b.extraIncome) + "/7d · " + statusLabel,
            statusColor
          );
        });
        var savedDays = daysBaseline - daysWithBridges;
        if (savedDays > 0) {
          html += nmRow(
            "Goal with bridges",
            "~" + daysWithBridges + "d (saves " + savedDays + "d)",
            c.green
          );
        }
      } else {
        html += nmRow("", "No bridgebuilder options", c.muted);
      }

      if (hasSell) {
        html += '<div style="margin:4px 0;border-top:1px dashed ' + c.divider + '"></div>';
        html += '<div style="font-size:9px;color:#5a6a7a;letter-spacing:0.08em;' + s + ';margin-bottom:5px">💸 Alt: Sell lower ROI blocks</div>';
        var cumulativeSale = 0;
        var cumulativeLostIncome = 0;
        sellCandidates.slice(0, 4).forEach(function(sc) {
          cumulativeSale += sc.saleValue;
          cumulativeLostIncome += sc.weekly;
          var daysIfSold = daysToAfford(target.cost - cumulativeSale, totalCapital, weeklyIncome - cumulativeLostIncome);
          html += nmRow("Sell " + sc.sym + " " + sc.tier + " · " + sc.roi.toFixed(1) + "%", fmRoi(sc.saleValue) + " → ~" + daysIfSold + "d", c.muted);
        });
      }

      // Alt: Wait
      html += '<div style="border-top:1px solid ' + c.divider + ';margin:6px 0"></div>';
      html += '<div style="font-size:9px;color:#5a6a7a;letter-spacing:0.08em;' + s + ';margin-bottom:5px">⏱ Alt: Wait</div>';
      html += nmRow("Income", fmRoi(weeklyIncome) + " / 7d (current benefits)");

      // Income breakdown
      var incomeBreakdown = [];
      Object.keys(ownedMap).forEach(function(sym) {
        var o = ownedMap[sym];
        if (!o.has_dividend || o.benefit_shares <= 0) return;
        var increments = o.dividend_increment || 0;
        if (increments <= 0) return;
        ROI_TABLE.forEach(function(entry) {
          if (entry.sym !== sym) return;
          var tierNum = parseInt(entry.tier.replace("T",""), 10);
          if (tierNum !== increments) return;
          var itemVal = getItemValue(entry);
          var weekly = (entry.payout + itemVal) / entry.freq * 7;
          incomeBreakdown.push(sym + " " + fmRoi(weekly));
        });
      });
      if (incomeBreakdown.length > 0) {
        html += nmRow("Breakdown", incomeBreakdown.slice(0,3).join(" · "));
        if (incomeBreakdown.length > 3) html += nmRow("", incomeBreakdown.slice(3,6).join(" · "));
      }
      html += nmRow("Goal in", daysWait === 0 ? "Now!" : daysWait === Infinity ? "N/A" : "~" + daysWait + " days", c.text);
      html += '</div>';
    }

    // Table header
    html += '<div style="display:grid;grid-template-columns:42px 26px 1fr 54px 32px;gap:4px;padding:5px 14px;font-size:9px;letter-spacing:0.1em;color:' + c.muted + ';text-transform:uppercase;border-bottom:1px solid ' + c.divider + ';' + s + ';position:sticky;top:0;background:' + c.bg + ';z-index:2;">' +
      '<span>Stock</span><span>Tier</span><span>Shares needed / Cost</span><span style="text-align:right">ROI</span><span></span></div>';


    // Show all benefit stocks: owned tiers first (green), then next tiers sorted by ROI
    var tableRows = [];
    // Add owned entries
    ownedEntries.forEach(function(e) {
      var o = ownedMap[e.sym];
      // Days remaining = frequency - progress (both already stored from API)
      var freq = (o && o.dividend_frequency) || e.freq || 0;
      var prog = (o && o.dividend_progress) || 0;
      var daysLeft = (freq > 0 && prog >= 0) ? Math.max(0, freq - prog) : -1;
      tableRows.push({ sym: e.sym, tier: e.tier, isOwned: true, cost: 0, roi: 0, sharesNeeded: 0, payout: e.payout || 0, freq: freq, daysLeft: daysLeft });
    });
    // Add next tier for each stock
    dynamicNextTiers.forEach(function(e) {
      var key = e.sym + "T" + e.tierInfo.nextIncrement;
      var isNext = nextEntries.some(function(ne) { return ne.sym === e.sym && ne.tier === "T" + e.tierInfo.nextIncrement; });
      var isBridge = bridgeChain.some(function(b) { return b.sym === e.sym && b.tier === "T" + e.tierInfo.nextIncrement; });
      var isSkipped = roiSkipped.indexOf(key) >= 0;
      tableRows.push({
        sym: e.sym,
        tier: "T" + e.tierInfo.nextIncrement,
        isOwned: false,
        isNext: isNext,
        isBridge: !!isBridge,
        isSkipped: isSkipped,
        cost: e.cost,
        roi: e.roi,
        sharesNeeded: e.tierInfo.sharesNeeded,
        payout: e.payoutEntry ? e.payoutEntry.payout : 0,
        freq: e.payoutEntry ? e.payoutEntry.freq : 7,
        item: e.payoutEntry ? e.payoutEntry.item : 0,
        key: key
      });
    });

    tableRows.forEach(function(row) {
      var rowBg = row.isOwned ? c.owned_bg : row.isNext ? c.next_bg : row.isBridge ? c.bridge_bg : row.isSkipped ? c.skip_bg : "transparent";
      var borderLeft = row.isOwned ? c.owned_border : row.isNext ? c.next_border : row.isBridge ? c.bridge_border : row.isSkipped ? c.skip_border : "transparent";
      var symColor = row.isOwned ? c.green : row.isNext ? c.blue : row.isBridge ? c.bridge : row.isSkipped ? c.red : c.neutral;
      var roiPct = row.isBridge ? "🔗 " + row.roi.toFixed(2) + "%" : row.roi > 0 ? row.roi.toFixed(2) + "%" : "—";
      var itemVal = row.item ? getItemValue({ sym: row.sym, item: row.item }) : 0;
      var nextPayoutStr = "";
      if (row.isOwned && row.daysLeft >= 0) {
        nextPayoutStr = row.daysLeft === 0 ? " · next <1d" : " · next " + row.daysLeft + "d";
      }
      var nextPayoutColor = (row.isOwned && row.daysLeft >= 0 && row.daysLeft <= 1) ? c.yellow : c.muted;
      var costLine = row.isOwned ? "Owned" :
        row.sharesNeeded.toLocaleString("en-US") + " shares · " + fmRoi(row.cost);
      var skipBtnStyle = 'width:28px;height:28px;border-radius:50%;border:1px solid ' + c.divider + ';background:none;cursor:pointer;font-size:10px;color:' + c.muted + ';display:flex;align-items:center;justify-content:center;justify-self:center;' + (row.isOwned ? 'opacity:0.2;pointer-events:none;' : '');
      var skipLabel = row.isSkipped ? "↩" : "✕";
      var key = row.key || (row.sym + row.tier);

      var buyAttrs = (!row.isOwned && row.sharesNeeded > 0)
        ? ' class="tsa-roi-buyrow" data-buy-sym="' + row.sym + '" data-buy-shares="' + row.sharesNeeded + '" data-buy-tier="' + row.tier + '" data-buy-state="0"'
        : '';
      html += '<div data-roi-key="' + key + '"' + buyAttrs + ' style="display:grid;grid-template-columns:42px 26px 1fr 54px 32px;gap:4px;align-items:center;padding:7px 14px;border-bottom:1px solid ' + c.row_border + ';background:' + rowBg + ';border-left:2px solid ' + borderLeft + ';cursor:' + (buyAttrs ? 'pointer' : 'default') + ';transition:background 0.15s">' +
        '<span style="' + s + ';font-weight:700;font-size:12px;color:' + symColor + '">' + row.sym + '</span>' +
        '<span style="' + s + ';font-size:9px;color:' + c.muted + '">' + row.tier + '</span>' +
        '<div style="display:flex;flex-direction:column;gap:1px;overflow:hidden;min-width:0">' +
          '<span style="' + s + ';font-size:10px;color:' + c.muted + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + costLine + (nextPayoutStr ? '<span style="color:' + nextPayoutColor + '">' + nextPayoutStr + '</span>' : '') + '</span>' +
          '<span style="font-size:9px;color:' + c.muted + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + fmRoi(row.payout) + (itemVal > 0 ? " + " + fmRoi(itemVal) : "") + " / " + row.freq + "d</span>" +
        '</div>' +
        '<span style="' + s + ';font-size:11px;font-weight:700;text-align:right;color:' + symColor + '">' + roiPct + '</span>' +
        '<button class="tsa-roi-skip" data-key="' + key + '" data-owned="' + (row.isOwned?1:0) + '" style="' + skipBtnStyle + '">' + skipLabel + '</button>' +
        '</div>';
    });

    return html;
  }

  function showROIPlanner(ownedMap, raw) {
    var content = document.getElementById("tsa-content");
    if (!content) return;
    var isDarkNow = document.getElementById("tsa-overlay").classList.contains("tsa-dark");
    content.style.background = isDarkNow ? "#0f0f1a" : "#ffffff";
    content.style.color = isDarkNow ? "#c8c8d8" : "#222";
    content.innerHTML = '<div style="padding:20px;text-align:center;color:' + (isDarkNow ? '#7a7a9a' : '#888') + ';font-size:12px"><span class="tsa-spinner"></span>Loading...</div>';

    // Shared listener setup — used by both success and catch paths
    function attachListeners() {
      content.querySelectorAll(".tsa-roi-skip").forEach(function(btn) {
        btn.addEventListener("click", function(e) {
          e.stopPropagation();
          if (btn.dataset.owned === "1") return;
          var k = btn.dataset.key;
          var idx = roiSkipped.indexOf(k);
          if (idx >= 0) roiSkipped.splice(idx, 1);
          else roiSkipped.push(k);
          lsSet("tsa_roi_skipped", JSON.stringify(roiSkipped));
          showROIPlanner(ownedMap, raw);
        });
      });
      content.querySelectorAll(".tsa-roi-buyrow").forEach(function(row) {
        row.addEventListener("click", function(e) {
          if (e.target.closest(".tsa-roi-skip")) return;
          var sym    = row.dataset.buySym;
          var shares = parseInt(row.dataset.buyShares, 10);
          var tier   = row.dataset.buyTier;
          var state  = row.dataset.buyState || "0";
          if (state === "0") {
            row.dataset.buyState = "1";
            row.style.background = "rgba(76,145,255,0.13)";
            row.style.borderLeft = "2px solid #4c91ff";
            var infoCol = row.querySelector("div");
            if (infoCol) {
              infoCol.innerHTML =
                '<span style="font-size:10px;font-weight:700;color:#4c91ff;font-family:JetBrains Mono,monospace">▲ Tap again to confirm</span>' +
                '<span style="font-size:9px;color:#4c91ff;font-family:JetBrains Mono,monospace">' + shares.toLocaleString("en-US") + ' shares · ' + tier + '</span>';
            }
            setTimeout(function() {
              if (row.dataset.buyState === "1" && roiPlannerActive) {
                row.dataset.buyState = "0";
                showROIPlanner(ownedMap, raw);
              }
            }, 4000);
          } else {
            row.dataset.buyState = "0";
            qtBuildMaps();
            qtPostTrade(sym, shares, "buyShares", "Bought " + shares.toLocaleString("en-US") + " " + sym + " (" + tier + ")");
            showROIPlanner(ownedMap, raw);
          }
        });
      });
    }

    function renderAndAttach(cashBalance, armoryFunds) {
      fetchAllItemPrices(function() {
        content.innerHTML = renderROIPlanner(ownedMap, raw, cashBalance, armoryFunds);
        attachListeners();
        var isDarkFooter = document.getElementById("tsa-overlay").classList.contains("tsa-dark");
        var footerDivider = isDarkFooter ? "#1a1a2e" : "#eee";
        var footerBg      = isDarkFooter ? "#0f0f1a" : "#ffffff";
        content.insertAdjacentHTML("beforeend",
          '<div style="padding:7px 14px;display:flex;justify-content:space-between;align-items:center;border-top:1px solid ' + footerDivider + ';background:' + footerBg + '">' +
            '<span style="font-size:9px;color:#555">✕ skip &nbsp;·&nbsp; ↩ unskip</span>' +
            '<span style="font-size:9px;color:#555;font-family:monospace">Updated ' + new Date().toLocaleTimeString("en-GB") + '</span>' +
          '</div>'
        );
      });
    }

    var key = getTornKey();
    Promise.all([
      fetchJSON("https://api.torn.com/user/?selections=basic,money&key=" + key).catch(function() { return null; }),
      fetchJSON("https://api.torn.com/faction/?selections=donations&key=" + key).catch(function() { return null; })
    ]).then(function(results) {
      var cashBalance = 0;
      var armoryFunds = 0;
      var userData = results[0];
      var factionData = results[1];
      if (userData && !userData.error) {
        cashBalance = userData.money_onhand || 0;
        var playerId = userData.player_id;
        if (playerId && factionData && factionData.donations && factionData.donations[playerId]) {
          armoryFunds = factionData.donations[playerId].money_balance || 0;
        }
      }
      renderAndAttach(cashBalance, armoryFunds);
    }).catch(function() {
      renderAndAttach(0, 0);
    });
  }

  function injectStyles() {
    var el = document.createElement("style");
    el.textContent = STYLES;
    document.head.appendChild(el);
  }

  function fetchJSON(url, retries) {
    if (retries === undefined) retries = 3;
    return new Promise(function(resolve, reject) {
      var attempt = function(n) {
        GM_xmlhttpRequest({
          method: "GET", url: url,
          onload: function(r) {
            try { resolve(JSON.parse(r.responseText)); }
            catch (e) {
              if (n > 1) { setTimeout(function() { attempt(n - 1); }, 2000); }
              else reject(new Error("Parse error: " + r.responseText.substring(0, 80)));
            }
          },
          onerror: function() {
            if (n > 1) { setTimeout(function() { attempt(n - 1); }, 2000); }
            else reject(new Error("Network error after 3 attempts: " + url));
          }
        });
      };
      attempt(retries);
    });
  }

  function buildOwnedMap(tornData) {
    var owned = {};
    if (!tornData || !tornData.stocks) return owned;
    Object.keys(tornData.stocks).forEach(function(id) {
      var s = tornData.stocks[id];
      var acronym = STOCK_ID_MAP[parseInt(id, 10)];
      if (!acronym) return;
      var transactions = s.transactions ? Object.keys(s.transactions).map(function(k) { return s.transactions[k]; }) : [];
      if (transactions.length === 0) return;
      // Total shares from ALL transactions regardless of bought_price
      var totalShares = 0, earliestTime = Infinity;
      transactions.forEach(function(t) {
        totalShares += t.shares || 0;
        if (t.time_bought && t.time_bought < earliestTime) earliestTime = t.time_bought;
      });
      // avg_price: only transactions with bought_price > 0
      // Torn API returns bought_price=0 for old/benefit-era purchases — skip those
      var validCostShares = 0, totalCost = 0;
      transactions.forEach(function(t) {
        if (t.bought_price && t.bought_price > 0) {
          validCostShares += t.shares || 0;
          totalCost += (t.shares || 0) * t.bought_price;
        }
      });
      var avg_price = validCostShares > 0 ? totalCost / validCostShares : 0;

      // Use Torn API's increment field — it is the active/confirmed tier
      var apiIncrement = (s.dividend && s.dividend.increment) || 0;

      // benefit_shares and swing_shares will be calculated by enrichOwnedMap
      // after live prices are available — store raw data only for now
      owned[acronym] = {
        shares: totalShares,
        swing_shares: 0,         // recalculated in enrichOwnedMap
        benefit_shares: 0,       // recalculated in enrichOwnedMap
        avg_price: avg_price,
        time_bought: earliestTime === Infinity ? null : earliestTime,
        has_dividend: apiIncrement > 0,
        has_swing: false,        // recalculated in enrichOwnedMap
        dividend_progress: (s.dividend && s.dividend.progress) || 0,
        dividend_frequency: (s.dividend && s.dividend.frequency) || 0,
        dividend_increment: apiIncrement,
        dividend_next: 0, // not exposed by API — calculated from progress/frequency
        transactions: transactions.sort(function(a, b) { return b.time_bought - a.time_bought; })
      };
    });
    return owned;
  }

  // Enrich ownedMap with correct benefit_shares and swing_shares
  // Uses BENEFIT_REQ (fixed share counts per BB — Torn game mechanic, never changes)
  // and dividend.increment from API
  // Formula: benefit_shares = BENEFIT_REQ[sym] × (2^increment - 1)
  // swing_shares = total_shares - benefit_shares
  function enrichOwnedMap(ownedMap, raw) {
    Object.keys(ownedMap).forEach(function(sym) {
      var o = ownedMap[sym];
      var increment = o.dividend_increment || 0;
      var totalShares = o.shares || 0;

      if (increment <= 0) {
        o.benefit_shares = 0;
        o.swing_shares = totalShares;
        o.has_dividend = false;
        o.has_swing = totalShares > 0;
        return;
      }

      var req = BENEFIT_REQ[sym] || 0;
      // If user has bought enough shares for a higher tier (pending next dividend day),
      // treat those extra shares as BB rather than swing.
      var effectiveIncrement = increment;
      if (req > 0) {
        while (totalShares >= (Math.pow(2, effectiveIncrement + 1) - 1) * req) {
          effectiveIncrement++;
        }
      }
      var benefitShares = req > 0 ? (Math.pow(2, effectiveIncrement) - 1) * req : 0;
      benefitShares = Math.min(benefitShares, totalShares);
      var swingShares = Math.max(0, totalShares - benefitShares);

      o.benefit_shares = benefitShares;
      o.swing_shares = swingShares;
      o.has_dividend = benefitShares > 0;
      o.has_swing = swingShares > 0;
    });
    return ownedMap;
  }

  function mergeIntervals(calls) {
    var merged = {};
    calls.forEach(function(call) {
      var stocks = call.data || call;
      if (!Array.isArray(stocks)) return;
      stocks.forEach(function(s) {
        if (!merged[s.stock]) {
          merged[s.stock] = Object.assign({}, s, {interval: {}});
        }
        Object.assign(merged[s.stock].interval, s.interval || {});
        if (s.price) merged[s.stock].price = s.price;
        if (s.investors) merged[s.stock].investors = s.investors;
      });
    });
    return Object.values(merged);
  }

  function calcScore(stock, raw, ownedMap, priceHistory) {
    var s = stock.toUpperCase();
    var r = raw ? raw.find(function(x) { return x.stock === s; }) : null;
    if (!r) return null;

    var owned = ownedMap[s];
    var isInBenefitCategory = owned && owned.has_dividend && owned.benefit_shares > 0;
    var p_live = parseFloat(r.price) || 0;

    // Extract all intervals
    var p_m30 = parseFloat((r.interval && r.interval.m30 && r.interval.m30.price)) || 0;
    var p_h1  = parseFloat((r.interval && r.interval.h1  && r.interval.h1.price))  || 0;
    var p_h2  = parseFloat((r.interval && r.interval.h2  && r.interval.h2.price))  || 0;
    var p_h3  = parseFloat((r.interval && r.interval.h3  && r.interval.h3.price))  || 0;
    var p_h4  = parseFloat((r.interval && r.interval.h4  && r.interval.h4.price))  || 0;
    var p_h6  = parseFloat((r.interval && r.interval.h6  && r.interval.h6.price))  || 0;
    var p_h8  = parseFloat((r.interval && r.interval.h8  && r.interval.h8.price))  || 0;
    var p_h12 = parseFloat((r.interval && r.interval.h12 && r.interval.h12.price)) || 0;
    var p_h16 = parseFloat((r.interval && r.interval.h16 && r.interval.h16.price)) || 0;
    var p_h20 = parseFloat((r.interval && r.interval.h20 && r.interval.h20.price)) || 0;
    var p_d1  = parseFloat((r.interval && r.interval.d1  && r.interval.d1.price))  || 0;
    var p_d2  = parseFloat((r.interval && r.interval.d2  && r.interval.d2.price))  || 0;
    var p_d3  = parseFloat((r.interval && r.interval.d3  && r.interval.d3.price))  || 0;
    var p_d4  = parseFloat((r.interval && r.interval.d4  && r.interval.d4.price))  || 0;
    var p_d5  = parseFloat((r.interval && r.interval.d5  && r.interval.d5.price))  || 0;
    var p_d7  = parseFloat((r.interval && r.interval.d7  && r.interval.d7.price))  || 0;
    var p_w1  = parseFloat((r.interval && r.interval.w1  && r.interval.w1.price))  || 0;

    var score = 0;
    var reasons = [];
    var scoreBreakdown = { drop: 0, position: 0, reversal: 0, macd: 0, rsi: 0 };

    // ── HARD FILTER: must be below weekly level ───────────────────────
    // Live must be below w1 — mandatory, no score if fails
    if (p_w1 > 0 && p_live >= p_w1) {
      return {
        symbol: s, score: 0, signal: "WAIT", sellSignal: null,
        scoreBreakdown: { drop: 0, position: 0, reversal: 0 },
        owned: !!owned, alreadyRallied: false, priceAboveWeek: true,
        has_swing: (owned && owned.has_swing) || false,
        has_benefit: (owned && owned.has_dividend) || false,
        hasDividend: (owned && owned.has_dividend) || false,
        dividendProgress: (owned && owned.dividend_progress) || 0,
        dividendFrequency: (owned && owned.dividend_frequency) || 0,
        p_live, reasons: "Above weekly level",
        netProfitPct: (owned && owned.avg_price > 0) ? ((p_live * 0.999 - owned.avg_price) / owned.avg_price * 100) : null,
        hoursHeld: (owned && owned.time_bought) ? ((Date.now()/1000 - owned.time_bought)/3600).toFixed(0) : null,
        shares: (owned && owned.shares) || 0,
        avg_price: (owned && owned.avg_price) || 0,
        transactions: (owned && owned.transactions) || []
      };
    }

    // ── 1. DROP FROM WEEKLY PEAK (max 60p) ───────────────────────────
    // How far has price dropped from its weekly high?
    // Drop threshold is dynamic: based on stock's average daily volatility (1x)
    var weekPrices = [p_d1,p_d2,p_d3,p_d4,p_d5,p_d7,p_w1].filter(function(x){ return x > 0; });
    var dropFromWeekPeak = 0;

    // Calculate dynamic drop threshold from localStorage history (avg hourly % change × 24)
    var dynamicDropThreshold = -1.0; // fallback
    var histPrices = priceHistory && priceHistory[s.toUpperCase()];
    if (histPrices && histPrices.length >= 48) {
      var sortedHist = histPrices.slice().sort(function(a,b){ return a.ts - b.ts; });
      var hPrices = sortedHist.map(function(e){ return e.price; });
      var changes = [];
      for (var hi = 1; hi < hPrices.length; hi++) {
        if (hPrices[hi-1] > 0) changes.push(Math.abs(hPrices[hi] - hPrices[hi-1]) / hPrices[hi-1] * 100);
      }
      if (changes.length > 0) {
        var avgHourlyVol = changes.reduce(function(a,b){ return a+b; }, 0) / changes.length;
        dynamicDropThreshold = -Math.max(0.5, avgHourlyVol * 24);
      }
    }

    if (weekPrices.length > 0) {
      var weekPeak = Math.max.apply(null, weekPrices);
      dropFromWeekPeak = ((p_live - weekPeak) / weekPeak) * 100;
      // Score relative to dynamic threshold
      var dt = dynamicDropThreshold; // negative value e.g. -1.2
      if      (dropFromWeekPeak <= dt * 4.0) { score += 60; scoreBreakdown.drop = 60; reasons.push("Drop " + dropFromWeekPeak.toFixed(1) + "%"); }
      else if (dropFromWeekPeak <= dt * 2.5) { score += 50; scoreBreakdown.drop = 50; reasons.push("Drop " + dropFromWeekPeak.toFixed(1) + "%"); }
      else if (dropFromWeekPeak <= dt * 1.5) { score += 40; scoreBreakdown.drop = 40; reasons.push("Drop " + dropFromWeekPeak.toFixed(1) + "%"); }
      else if (dropFromWeekPeak <= dt * 1.0) { score += 30; scoreBreakdown.drop = 30; reasons.push("Drop " + dropFromWeekPeak.toFixed(1) + "%"); }
      else if (dropFromWeekPeak <= dt * 0.5) { score += 15; scoreBreakdown.drop = 15; reasons.push("Drop " + dropFromWeekPeak.toFixed(1) + "%"); }
      else                                   {              reasons.push("Drop " + dropFromWeekPeak.toFixed(1) + "%"); }
    }

    // ── 2. NEAR SHORT-TERM BOTTOM (max 35p) ──────────────────────────
    // Is live near the bottom of its recent (h1-d2) range?
    var shortPrices = [p_h1,p_h2,p_h3,p_h4,p_h6,p_h8,p_h12,p_h16,p_h20,p_d1,p_d2].filter(function(x){ return x > 0; });
    if (shortPrices.length >= 2) {
      var shortLow  = Math.min.apply(null, shortPrices);
      var shortHigh = Math.max.apply(null, shortPrices);
      var shortRange = shortHigh - shortLow;
      if (shortRange > 0) {
        var posInShort = Math.max(0, ((p_live - shortLow) / shortRange) * 100);
        if      (posInShort <= 10) { score += 35; scoreBreakdown.position = 35; reasons.push("Near bottom " + posInShort.toFixed(0) + "%"); }
        else if (posInShort <= 25) { score += 25; scoreBreakdown.position = 25; reasons.push("Low pos " + posInShort.toFixed(0) + "%"); }
        else if (posInShort <= 40) { score += 15; scoreBreakdown.position = 15; reasons.push("Pos " + posInShort.toFixed(0) + "%"); }
        else if (posInShort <= 55) { score += 5;  scoreBreakdown.position = 5;  reasons.push("Pos " + posInShort.toFixed(0) + "%"); }
        else                       {              reasons.push("High pos " + posInShort.toFixed(0) + "%"); }
      }
    }

    // ── 3. SHORT-TERM PRICE MOVEMENT (max 40p) ───────────────────────
    // Is price actively rising right now? m30 vs h1 vs h2
    var m30Valid = p_m30 > 0, h1Valid = p_h1 > 0, h2Valid = p_h2 > 0;
    if (m30Valid && h1Valid && h2Valid) {
      var rising_m30_h1 = p_m30 > p_h1;
      var rising_h1_h2 = p_h1 > p_h2;
      if (rising_m30_h1 && rising_h1_h2) {
        score += 40; scoreBreakdown.reversal = 40; reasons.push("Active rise");
      } else if (rising_m30_h1) {
        score += 20; scoreBreakdown.reversal = 20; reasons.push("Rising +" + ((p_m30 - p_h1) / p_h1 * 100).toFixed(2) + "%");
      } else if (Math.abs(p_m30 - p_h1) / p_h1 * 100 <= 0.1) {
        score += 8; scoreBreakdown.reversal = 8; reasons.push("Stabilizing");
      } else {
        reasons.push("Still falling " + ((p_m30 - p_h1) / p_h1 * 100).toFixed(2) + "%");
      }
    } else if (m30Valid && h1Valid) {
      if (p_m30 > p_h1) {
        score += 20; scoreBreakdown.reversal = 20; reasons.push("Rising");
      } else {
        reasons.push("Falling");
      }
    }

    // ── 4. MACD (max 25p) ────────────────────────────────────────────
    var macdResult = calcMACD(s, priceHistory);
    if (macdResult !== null) {
      if (macdResult.crossover) {
        score += 25; scoreBreakdown.macd = 25; reasons.push("MACD crossover");
      } else if (macdResult.macd > macdResult.signal) {
        score += 12; scoreBreakdown.macd = 12; reasons.push("MACD bullish");
      } else {
        reasons.push("MACD bearish");
      }
    }

    // ── 5. RSI (max 20p) — Torn-calibrated via percentile ───────────
    // Uses the stock's own RSI history to define "oversold" for that specific
    // stock, instead of generic 30/70 real-market thresholds.
    var rsiCtx = calcRSIContext(s, priceHistory);
    var rsi = rsiCtx ? rsiCtx.rsi : null;
    var rsiPercentile = rsiCtx ? rsiCtx.percentile : null;

    if (rsiPercentile !== null) {
      // Percentile-based: how low is current RSI vs this stock's own RSI history
      if      (rsiPercentile <= 10) { score += 20; scoreBreakdown.rsi = 20; reasons.push("RSI " + rsi.toFixed(0) + " (btm 10%)"); }
      else if (rsiPercentile <= 25) { score += 12; scoreBreakdown.rsi = 12; reasons.push("RSI " + rsi.toFixed(0) + " (btm 25%)"); }
      else if (rsiPercentile <= 40) { score += 6;  scoreBreakdown.rsi = 6;  reasons.push("RSI " + rsi.toFixed(0)); }
      else                          {                                         reasons.push("RSI " + rsi.toFixed(0)); }
    } else if (rsi !== null) {
      // Fallback: absolute RSI with Torn-adjusted thresholds (conservative — Torn
      // stocks lack the liquidity floor that real markets provide)
      if      (rsi <= 35) { score += 15; scoreBreakdown.rsi = 15; reasons.push("RSI " + rsi.toFixed(0)); }
      else if (rsi <= 45) { score += 7;  scoreBreakdown.rsi = 7;  reasons.push("RSI " + rsi.toFixed(0)); }
      else                {                                         reasons.push("RSI " + rsi.toFixed(0)); }
    }

    // ── HARD FILTERS ─────────────────────────────────────────────────
    var recentPrices = [p_h1,p_h2,p_h4,p_h8,p_h12,p_d1].filter(function(x){ return x > 0; });
    var recentLow = recentPrices.length > 0 ? Math.min.apply(null, recentPrices) : 0;
    // Already rallied if price has recovered more than half of the weekly drop
    var rallyPct = recentLow > 0 && p_live > recentLow ? ((p_live - recentLow) / recentLow * 100) : 0;
    var rallyThreshold = dropFromWeekPeak < 0 ? Math.abs(dropFromWeekPeak) / 2 : 0.3;
    var alreadyRallied = rallyPct > rallyThreshold;
    // RSI overbought: use top-85th percentile when available, else absolute >65
    var rsiOverbought = rsiPercentile !== null ? rsiPercentile >= 85 : (rsi !== null && rsi > 65);

    // SIGNAL — requires MACD crossover (25p) AND score >= 100 for STRONG BUY
    var signal;
    if (score >= 100 && scoreBreakdown.macd === 25) signal = "STRONG BUY";
    else if (score >= 75) signal = "BUY";
    else if (score >= 45) signal = "CONSIDER";
    else                  signal = "WAIT";

    // SELL LOGIC
    var sellSignal = null, netProfitPct = null, hoursHeld = null;
    var isBenefitBlock = isInBenefitCategory;

    // Calculate profit % for ALL owned stocks — include 0.1% sell fee for accurate P/L
    if (owned && owned.avg_price > 0) {
      netProfitPct = ((p_live * 0.999 - owned.avg_price) / owned.avg_price * 100);
      hoursHeld = owned.time_bought
        ? ((Date.now() / 1000 - owned.time_bought) / 3600).toFixed(0)
        : null;
    }

    // Sell signal ONLY for swing trades
    if (owned && owned.avg_price > 0 && !isBenefitBlock) {
      var profitTarget = getProfitTarget();
      var stopLossThreshold = getStopLoss();
      if (netProfitPct >= profitTarget)              sellSignal = "PROFIT";
      else if (netProfitPct <= -stopLossThreshold)   sellSignal = "STOP LOSS";
    }

    return {
      symbol: s, score, signal, sellSignal,
      scoreBreakdown: scoreBreakdown,
      owned: !!owned,
      alreadyRallied: alreadyRallied,
      rsiOverbought: rsiOverbought,
      priceAboveWeek: p_w1 > 0 && p_live > p_w1,
      has_swing: (owned && owned.has_swing) || false,
      has_benefit: (owned && owned.has_dividend) || false,
      hasDividend: (owned && owned.has_dividend) || false,
      dividendProgress: (owned && owned.dividend_progress) || 0,
      dividendFrequency: (owned && owned.dividend_frequency) || 0,
      p_live, reasons: reasons.join(" | "), netProfitPct, hoursHeld,
      shares: (owned && owned.shares) || 0,
      avg_price: (owned && owned.avg_price) || 0,
      transactions: (owned && owned.transactions) || []
    };
  }

  function getProfitTarget() {
    return parseFloat(lsGet("tsa_profit_target", "0.3"));
  }

  function getStopLoss() {
    return parseFloat(lsGet("tsa_stop_loss", "1.0"));
  }

  function escHtml(s) {
    return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
  }

  function showToast(msg, type) {
    var colors = {
      success: { bg:"rgba(20,160,70,0.93)",  border:"#4cff91", icon:"✓" },
      error:   { bg:"rgba(170,20,50,0.93)",  border:"#ff4c6a", icon:"✕" },
      warn:    { bg:"rgba(150,110,0,0.93)",  border:"#ffc107", icon:"!" },
      info:    { bg:"rgba(40,70,140,0.93)",  border:"#7a9fd4", icon:"i" }
    };
    var c = colors[type] || colors.info;
    var toast = document.createElement("div");
    toast.style.cssText = "position:fixed;bottom:16px;left:16px;z-index:2147483648;" +
      "max-width:280px;padding:10px 14px;border-radius:10px;border-left:3px solid " + c.border + ";" +
      "background:" + c.bg + ";color:#fff;font-family:JetBrains Mono,monospace;font-size:12px;" +
      "font-weight:600;box-shadow:0 4px 16px rgba(0,0,0,0.4);display:flex;align-items:center;" +
      "gap:8px;animation:tsaToastIn 0.2s ease";
    toast.innerHTML = "<span style='font-size:14px;flex-shrink:0'>" + c.icon + "</span><span>" + escHtml(msg) + "</span>";
    document.body.appendChild(toast);
    setTimeout(function() {
      toast.style.opacity = "0";
      toast.style.transition = "opacity 0.3s";
      setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 320);
    }, 3000);
  }

  function updateCountdownLabel() {
    var el = document.getElementById("tsa-countdown");
    if (!el) return;
    if (!autoRefreshEndTime) { el.textContent = ""; return; }
    var rem = Math.max(0, autoRefreshEndTime - Date.now());
    var m = Math.floor(rem / 60000);
    var s = Math.floor((rem % 60000) / 1000);
    el.textContent = " · " + m + "m " + (s < 10 ? "0" : "") + s + "s";
  }

  var API_ERRORS = {
    "Incorrect key":  "Invalid API key — click 🔑 to update it.",
    "Access level":   "API key is missing required permissions — click 🔑 to create a new key.",
    "Too many":       "Too many requests — wait a moment and try again.",
    "Player not found": "Player profile not found. Check the API key."
  };

  function friendlyApiError(msg) {
    var keys = Object.keys(API_ERRORS);
    for (var i = 0; i < keys.length; i++) {
      if (msg && msg.indexOf(keys[i]) !== -1) return API_ERRORS[keys[i]];
    }
    return msg;
  }

  function getTsaStorageSize() {
    var total = 0;
    try {
      Object.keys(localStorage).forEach(function(key) {
        if (key.indexOf("tsa") === 0 || key.indexOf("qt_") === 0) {
          total += (lsGet(key) || "").length;
        }
      });
    } catch(e) {}
    if (total < 1024) return total + " B";
    if (total < 1024 * 1024) return (total / 1024).toFixed(1) + " KB";
    return (total / (1024 * 1024)).toFixed(2) + " MB";
  }

  function getProfitSwingOnly() {
    return lsGet("tsa_profit_swing_only", "true") !== "false";
  }
  function getShowWatch() {
    return lsGet("tsa_show_watch", "true") !== "false";
  }
  function getTop5MinScore() {
    var v = parseInt(lsGet("tsa_top5_min_score", "35"), 10);
    return isNaN(v) ? 35 : v;
  }
  function getRequirePositiveInvestors() {
    return lsGet("tsa_require_positive_investors", "false") === "true";
  }
  function getShowRealized() {
    return lsGet("tsa_show_realized", "false") === "true";
  }

  function getOverlayPosition() {
    return lsGet("tsa_overlay_position", "bottom-right");
  }

  function applyOverlayPosition(pos) {
    var p = {
      "bottom-right": { btn: { bottom:"80px",  top:"auto", right:"16px", left:"auto" }, overlay: { bottom:"130px", top:"auto", right:"16px", left:"auto" } },
      "bottom-left":  { btn: { bottom:"80px",  top:"auto", right:"auto", left:"16px" }, overlay: { bottom:"130px", top:"auto", right:"auto", left:"16px" } },
      "top-right":    { btn: { bottom:"auto",  top:"16px", right:"16px", left:"auto" }, overlay: { bottom:"auto",  top:"60px", right:"16px", left:"auto" } },
      "top-left":     { btn: { bottom:"auto",  top:"16px", right:"auto", left:"16px" }, overlay: { bottom:"auto",  top:"60px", right:"auto", left:"16px" } }
    }[pos] || { btn: { bottom:"80px", top:"auto", right:"16px", left:"auto" }, overlay: { bottom:"130px", top:"auto", right:"16px", left:"auto" } };
    var btn = document.getElementById("tsa-btn");
    var ov  = document.getElementById("tsa-overlay");
    if (btn) { btn.style.bottom = p.btn.bottom; btn.style.top = p.btn.top; btn.style.right = p.btn.right; btn.style.left = p.btn.left; }
    if (ov)  { ov.style.bottom  = p.overlay.bottom; ov.style.top = p.overlay.top; ov.style.right = p.overlay.right; ov.style.left = p.overlay.left; }
  }
  function getRealizedDays() {
    return parseInt(lsGet("tsa_realized_days", "7"), 10);
  }
  function getRealizedEvents() {
    try { return JSON.parse(localStorage.getItem("tsa_realized_events") || "[]"); }
    catch(e) { return []; }
  }
  function getRealizedTotal() {
    var days = getRealizedDays();
    var cutoff = (Date.now() / 1000) - (days * 86400);
    return getRealizedEvents()
      .filter(function(e) { return e.ts >= cutoff; })
      .reduce(function(sum, e) { return sum + (e.profit || 0); }, 0);
  }

  function getRealizedByStock() {
    var days = getRealizedDays();
    var cutoff = (Date.now() / 1000) - (days * 86400);
    var byStock = {};
    getRealizedEvents()
      .filter(function(e) { return e.ts >= cutoff; })
      .forEach(function(e) { byStock[e.sym] = (byStock[e.sym] || 0) + (e.profit || 0); });
    return byStock;
  }

  // Remove buy intents older than 24 hours to avoid stale slippage warnings
  function cleanOldIntents() {
    var cutoff = Math.floor(Date.now() / 1000) - 86400;
    try {
      Object.keys(localStorage).forEach(function(key) {
        if (key.indexOf("qt_intent_") !== 0) return;
        var v = JSON.parse(localStorage.getItem(key) || "null");
        if (!v || v.ts < cutoff) localStorage.removeItem(key);
      });
    } catch(e) {}
  }

  function loadData() {
    cleanOldIntents();
    var content = document.getElementById("tsa-content");
    var isDarkMode = document.getElementById("tsa-overlay").classList.contains("tsa-dark");
    content.style.background = isDarkMode ? "#0f0f1a" : "#ffffff";

    // Check for API key
    var key = getTornKey();
    if (!key || key === "###PDA-APIKEY###") {
      showKeyOnboarding(content, function() { loadData(); });
      return;
    }

    content.innerHTML = "<div class=\"tsa-loading\">Fetching data...</div>";

    Promise.all([
      fetchJSON("https://api.torn.com/user/?selections=stocks&key=" + getTornKey()),
      fetchJSON("https://tornsy.com/api/stocks?interval=m30,h1,h2,h3,h4"),
      fetchJSON("https://tornsy.com/api/stocks?interval=h6,h8,h10,h12,h16"),
      fetchJSON("https://tornsy.com/api/stocks?interval=h20,d1,d2,d3,d4"),
      fetchJSON("https://tornsy.com/api/stocks?interval=d5,d6,d7,w1,h5")
    ]).then(function(results) {
      var tornData = results[0];
      var t1 = results[1];
      var t2 = results[2];
      var t3 = results[3];
      var t4 = results[4];

      if (tornData.error) { throw new Error(friendlyApiError(tornData.error.error)); }

      var ownedMap = buildOwnedMap(tornData);
      var ownedSymbols = Object.keys(ownedMap);
      var raw = mergeIntervals([t1, t2, t3, t4]);

      // Enrich ownedMap with correct benefit_shares/swing_shares using live prices
      enrichOwnedMap(ownedMap, raw);

      // Track realized profit from sold positions
      if (getShowRealized()) {
        try {
          var prevHoldings = JSON.parse(localStorage.getItem("tsa_prev_holdings") || "{}");
          var realizedEvents = getRealizedEvents();
          var nowTs = Math.floor(Date.now() / 1000);
          var prevSyms = Object.keys(prevHoldings);
          for (var rpi = 0; rpi < prevSyms.length; rpi++) {
            var rpSym = prevSyms[rpi];
            var prevEntry = prevHoldings[rpSym];
            if (!prevEntry) continue;
            var curEntry = ownedMap[rpSym];
            var prevShares = prevEntry.shares || 0;
            var curShares = curEntry ? (curEntry.shares || 0) : 0;
            var soldShares = prevShares - curShares;
            if (soldShares <= 0) continue;
            var rpRaw = raw ? raw.find(function(x) { return x.stock === rpSym; }) : null;
            var liveP = rpRaw ? (parseFloat(rpRaw.price) || 0) : 0;
            var costP = prevEntry.avg_price || 0;
            if (liveP <= 0 || costP <= 0) continue;
            var realizedProfit = (liveP * 0.999 - costP) * soldShares;
            realizedEvents.push({ ts: nowTs, profit: realizedProfit, sym: rpSym, sell_price: liveP });
          }
          // Trim events older than 90 days
          var trim90 = nowTs - 90 * 86400;
          realizedEvents = realizedEvents.filter(function(e) { return e.ts >= trim90; });
          localStorage.setItem("tsa_realized_events", JSON.stringify(realizedEvents));
        } catch(e) {}
        // Save current holdings snapshot for next comparison
        var holdingsSnap = {};
        Object.keys(ownedMap).forEach(function(s) {
          holdingsSnap[s] = { shares: ownedMap[s].shares || 0, avg_price: ownedMap[s].avg_price || 0 };
        });
        lsSet("tsa_prev_holdings", JSON.stringify(holdingsSnap));
      }

      // Store for ROI planner
      lastOwnedMap = ownedMap;
      lastRaw = raw;

      // Calculate best ROI recommendation for Quick Trade bar
      var benefitSymsForRec = Object.keys(BENEFIT_REQ);
      var recCandidates = [];
      benefitSymsForRec.forEach(function(sym) {
        var tierInfo = calcNextTier(sym, ownedMap, raw);
        if (!tierInfo || tierInfo.sharesNeeded <= 0) return;
        var payoutEntry = ROI_MAP[sym + "|T" + tierInfo.nextIncrement] || null;
        if (!payoutEntry) {
          // Fallback: find highest available tier for this sym
          for (var ri2 = ROI_TABLE.length - 1; ri2 >= 0; ri2--) {
            if (ROI_TABLE[ri2].sym === sym) { payoutEntry = ROI_TABLE[ri2]; break; }
          }
        }
        var roi = payoutEntry ? (payoutEntry.payout / tierInfo.cost * (365 / payoutEntry.freq) * 100) : 0;
        recCandidates.push({ sym: sym, tierInfo: tierInfo, cost: tierInfo.cost, roi: roi });
      });
      recCandidates.sort(function(a, b) { return b.roi - a.roi; });
      lastBestRec = recCandidates[0] || null;
      updateQtRecommendation(null);

      // Check price alerts
      checkAlerts(raw);

      // Record live prices to history — returns saved object to avoid re-parsing
      var _savedHistory = recordPrices(raw);

      // Update chart with selected stock
      var currentStock = lsGet("qt_last_stock", "");
      if (currentStock) qtDrawChart(currentStock);

      // If ROI planner is active, show it instead
      if (roiPlannerActive) { showROIPlanner(ownedMap, raw); return; }
      function safeParseArr(key) { try { return JSON.parse(localStorage.getItem(key) || "[]") || []; } catch(e) { return []; } }
      var hiddenStocks = safeParseArr("tsa_hidden");

      var cachedPriceHistory = _savedHistory || loadHistory();
      var stockResults = STOCKS_LIST
        .map(function(s) { return calcScore(s, raw, ownedMap, cachedPriceHistory); })
        .filter(Boolean)
        .sort(function(a, b) { return b.score - a.score; });

      // Log buy signals to history
      recordSignals(stockResults);

      // Snapshot prices for trend arrows on next load
      var prevPrices = {};
      Object.keys(lastLoadPrices).forEach(function(k) { prevPrices[k] = lastLoadPrices[k]; });
      stockResults.forEach(function(s) { if (s.p_live > 0) lastLoadPrices[s.symbol] = s.p_live; });

      var top5MinScore = getTop5MinScore();
      var cachedInvHistory = loadInvestorHistory();
      stockResults.forEach(function(s) {
        s.invDelta = getInvestorDelta(s.symbol, cachedInvHistory);
      });
      var top5BuyAll = stockResults.filter(function(s) {
        if (s.score < top5MinScore) return false;
        if (s.alreadyRallied) return false;
        if (s.priceAboveWeek) return false;
        if (s.rsiOverbought) return false;
        if (getRequirePositiveInvestors() && (s.invDelta === null || s.invDelta <= 0)) return false;
        if (!s.owned) return true;
        // Owned stocks only show in top 5 if BUY or above (score >= 75)
        return s.score >= 75;
      });
      // Pinned stocks bubble to top
      top5BuyAll.sort(function(a, b) {
        var ap = tsaPinned.indexOf(a.symbol) >= 0 ? 0 : 1;
        var bp = tsaPinned.indexOf(b.symbol) >= 0 ? 0 : 1;
        if (ap !== bp) return ap - bp;
        return b.score - a.score;
      });
      var top5Buy = top5BuyAll.slice(0, 5);

      // WATCH: all owned stocks with score 50-79 not already in top5Buy
      var watchList = stockResults.filter(function(s) {
        if (!s.owned) return false;
        if (s.score < 45 || s.score >= 75) return false;
        if (s.alreadyRallied) return false;
        if (s.priceAboveWeek) return false;
        if (s.rsiOverbought) return false;
        return !top5Buy.some(function(b) { return b.symbol === s.symbol; });
      });

      var fm = function(n) {
        var abs = Math.abs(n), sign = n < 0 ? "-" : "+";
        if (abs >= 1e9) return sign + "$" + (abs/1e9).toFixed(1) + "B";
        if (abs >= 1e6) return sign + "$" + (abs/1e6).toFixed(1) + "M";
        if (abs >= 1e3) return sign + "$" + (abs/1e3).toFixed(0) + "K";
        return sign + "$" + abs.toFixed(0);
      };

      var totalProfit = 0;
      var ownedKeys = Object.keys(ownedMap);
      for (var ki = 0; ki < ownedKeys.length; ki++) {
        var profitSym = ownedKeys[ki];
        var ownedEntry = ownedMap[profitSym];
        // Filter by swing-only setting
        if (getProfitSwingOnly() && (!ownedEntry.has_swing || ownedEntry.swing_shares <= 0)) continue;
        var rawEntry = raw ? raw.find(function(x) { return x.stock === profitSym; }) : null;
        if (!rawEntry) continue;
        var livePrice = parseFloat(rawEntry.price) || 0;
        if (livePrice <= 0) continue;

        // For mixed stocks (benefit + swing) in swing-only mode, count only the swing portion
        var isMixedStock = ownedEntry.has_dividend && ownedEntry.swing_shares > 0 && ownedEntry.benefit_shares > 0;
        if (getProfitSwingOnly() && isMixedStock) {
          var swingCost = ownedEntry.avg_price || 0;
          if (swingCost > 0) {
            totalProfit += (livePrice * 0.999 - swingCost) * ownedEntry.swing_shares;
          }
        } else {
          // Calculate profit per transaction using bought_price if available,
          // otherwise fall back to avg_price for transactions with bought_price=0
          var fallbackAvg = ownedEntry.avg_price || 0;
          var txProfit = 0;
          var txCounted = false;
          if (ownedEntry.transactions && ownedEntry.transactions.length > 0) {
            ownedEntry.transactions.forEach(function(t) {
              var shares = t.shares || 0;
              if (shares <= 0) return;
              var costPrice = (t.bought_price && t.bought_price > 0) ? t.bought_price : fallbackAvg;
              if (costPrice <= 0) return;
              txProfit += (livePrice * 0.999 - costPrice) * shares;
              txCounted = true;
            });
          }
          if (txCounted) totalProfit += txProfit;
        }
      }

      // Colour palette based on dark mode
      var isDark2 = document.getElementById("tsa-overlay").classList.contains("tsa-dark");
      var d = isDark2 ? {
        bg:"#0f0f1a", bg2:"#0d0d18", bg3:"#1a1a2e", border:"#2a2a4a",
        text:"#c8c8d8", muted:"#7a7a9a", blue:"#7a9fd4",
        green:"#4cff91", red:"#ff4c6a", yellow:"#ffc107",
        rowBuy:"rgba(76,255,145,0.08)", rowBuyBorder:"rgba(76,255,145,0.2)",
        rowSell:"rgba(255,76,106,0.08)", rowSellBorder:"rgba(255,76,106,0.2)",
        rowBenefit:"rgba(160,160,255,0.05)", rowBenefitBorder:"rgba(160,160,255,0.1)",
        divider:"#1a1a2e", txBg:"#0d0d18", txBorder:"#2a2a4a",
        moveBg:"rgba(255,193,7,0.1)", moveBorder:"rgba(255,193,7,0.3)", moveColor:"#ffc107",
        moveBg2:"rgba(122,159,212,0.1)", moveBorder2:"rgba(122,159,212,0.3)", moveColor2:"#7a9fd4",
        mono:"JetBrains Mono,monospace"
      } : {
        bg:"#ffffff", bg2:"#f7f9fc", bg3:"#f0f4ff", border:"#eee",
        text:"#222", muted:"#888", blue:"#4a6fa5",
        green:"#1a8a45", red:"#cc2222", yellow:"#856404",
        rowBuy:"#edfaf3", rowBuyBorder:"#a8e6c0",
        rowSell:"#fff0f0", rowSellBorder:"#ffb3b3",
        rowBenefit:"#f0f4ff", rowBenefitBorder:"#c0d0ff",
        divider:"#eee", txBg:"#f9f9f9", txBorder:"#e0e0e0",
        moveBg:"#fff3cd", moveBorder:"#ffc107", moveColor:"#856404",
        moveBg2:"#f0f4ff", moveBorder2:"#c0d0ff", moveColor2:"#4a6fa5",
        mono:"Arial,sans-serif"
      };
      var ms = "font-family:" + d.mono + ";";

      var html = "<div style=\"display:grid;grid-template-columns:repeat(3,1fr);gap:8px;padding:12px 14px;border-bottom:1px solid " + d.border + ";background:" + d.bg + "\">" +
        "<div style=\"background:" + d.bg2 + ";border-radius:8px;padding:8px;text-align:center;border:1px solid " + d.border + "\">" +
          "<div style=\"font-size:10px;color:" + d.muted + ";margin-bottom:4px\">Analyzed</div>" +
          "<div style=\"font-size:16px;font-weight:bold;color:" + d.text + ";" + ms + "\">" + stockResults.length + "</div></div>" +
        "<div style=\"background:" + d.bg2 + ";border-radius:8px;padding:8px;text-align:center;border:1px solid " + d.border + "\">" +
          "<div style=\"font-size:10px;color:" + d.muted + ";margin-bottom:4px\">You own</div>" +
          "<div style=\"font-size:16px;font-weight:bold;color:" + d.text + ";" + ms + "\">" + ownedSymbols.length + "</div></div>" +
        (function() {
          var showRealized = getShowRealized();
          var realizedTotal = showRealized ? getRealizedTotal() : 0;
          var realDays = getRealizedDays();
          if (showRealized) {
            var realExpanded = lsGet("tsa_realized_expanded", "false") === "true";
            var byStock = realExpanded ? getRealizedByStock() : {};
            var stockKeys = Object.keys(byStock).sort(function(a, b) { return Math.abs(byStock[b]) - Math.abs(byStock[a]); });
            var detailHtml = stockKeys.map(function(sym) {
              var p = byStock[sym];
              return "<div style=\"display:flex;justify-content:space-between;padding:1px 0;\">" +
                "<span style=\"font-size:9px;color:" + d.muted + "\">" + sym + "</span>" +
                "<span style=\"font-size:9px;font-weight:bold;color:" + (p >= 0 ? d.green : d.red) + ";" + ms + "\">" + fm(p) + "</span>" +
              "</div>";
            }).join("");
            return "<div style=\"background:" + d.bg2 + ";border-radius:8px;padding:8px;border:1px solid " + d.border + "\">" +
              "<div style=\"display:flex;justify-content:space-between;align-items:baseline;margin-bottom:3px\">" +
                "<div style=\"font-size:9px;color:" + d.muted + "\">Open P/L</div>" +
                "<div style=\"font-size:13px;font-weight:bold;color:" + (totalProfit >= 0 ? d.green : d.red) + ";" + ms + "\">" + fm(totalProfit) + "</div>" +
              "</div>" +
              "<div id=\"tsa-realized-row\" style=\"display:flex;justify-content:space-between;align-items:baseline;cursor:pointer;\">" +
                "<div style=\"font-size:9px;color:" + d.muted + "\">Real. " + realDays + "d " + (realExpanded ? "&#9660;" : "&#9658;") + "</div>" +
                "<div style=\"font-size:13px;font-weight:bold;color:" + (realizedTotal >= 0 ? d.green : d.red) + ";" + ms + "\">" + fm(realizedTotal) + "</div>" +
              "</div>" +
              (realExpanded && stockKeys.length > 0 ? "<div style=\"margin-top:4px;border-top:1px solid " + d.border + ";padding-top:4px\">" + detailHtml + "</div>" : "") +
            "</div>";
          } else {
            return "<div style=\"background:" + d.bg2 + ";border-radius:8px;padding:8px;text-align:center;border:1px solid " + d.border + "\">" +
              "<div style=\"font-size:10px;color:" + d.muted + ";margin-bottom:4px\">Trading profit</div>" +
              "<div style=\"font-size:16px;font-weight:bold;color:" + (totalProfit >= 0 ? d.green : d.red) + ";" + ms + "\">" + fm(totalProfit) + "</div></div>";
          }
        })() +
        "</div>";

      // Builds a "gap to next signal" hint line for the score breakdown panel.
      // Shows how many points are missing and which indicators have the most
      // unrealized potential to get there.
      function buildGapHtml(bd, score, signal) {
        var SIGNAL_THRESHOLDS = { "WAIT": 45, "CONSIDER": 75, "BUY": 100 };
        var nextLabel = signal === "WAIT" ? "CONSIDER" : signal === "CONSIDER" ? "BUY" : signal === "BUY" ? "STRONG BUY" : null;
        if (!nextLabel) return ""; // already STRONG BUY
        var nextThreshold = SIGNAL_THRESHOLDS[signal];
        var gap = nextThreshold - score;
        if (gap <= 0) return "";

        // Indicators sorted by unrealized potential (max - current)
        var indicators = [
          { key: "drop",     label: "stronger drop",       max: 60, pts: bd.drop     || 0 },
          { key: "reversal", label: "active price rise",   max: 40, pts: bd.reversal || 0 },
          { key: "position", label: "lower range position",max: 35, pts: bd.position || 0 },
          { key: "macd",     label: "MACD crossover",      max: 25, pts: bd.macd     || 0 },
          { key: "rsi",      label: "lower RSI",           max: 20, pts: bd.rsi      || 0 },
        ];
        indicators.sort(function(a, b) { return (b.max - b.pts) - (a.max - a.pts); });
        // Only mention indicators that can actually contribute meaningful points
        var useful = indicators.filter(function(i) { return (i.max - i.pts) >= 5; }).slice(0, 2);
        var needsStr = useful.length
          ? " — needs " + useful.map(function(i) { return i.label; }).join(" OR ")
          : "";

        var gapColor = gap <= 15 ? d.green : gap <= 30 ? d.yellow : d.muted;
        return "<div style=\"margin-bottom:4px;padding:5px 8px;border-radius:6px;" +
          "background:" + (isDark2 ? "rgba(122,159,212,0.07)" : "#f0f4ff") + ";" +
          "border:1px solid " + (isDark2 ? "rgba(122,159,212,0.15)" : "#c0d0ff") + "\">" +
          "<span style=\"font-size:10px;color:" + d.muted + "\">Gap to " + nextLabel + ": </span>" +
          "<span style=\"font-size:10px;font-weight:bold;color:" + gapColor + ";" + ms + "\">" + gap + "p</span>" +
          "<span style=\"font-size:10px;color:" + d.muted + "\">" + needsStr + "</span>" +
          "</div>";
      }

      if (top5Buy.length > 0) {
        html += "<div style=\"padding:10px 14px 6px;background:" + d.bg + "\">" +
          "<div style=\"font-size:10px;letter-spacing:0.12em;color:" + d.muted + ";text-transform:uppercase;margin-bottom:8px;font-weight:bold\">" +
          "Top " + top5Buy.length + " buy</div>";
        top5Buy.forEach(function(s) {
          var breakdownId = "tsa-breakdown-" + s.symbol;
          var bd = s.scoreBreakdown || {};
          var invDelta = s.invDelta;
          var invHtml = "";
          if (invDelta !== null) {
            var invColor = invDelta > 0 ? d.green : invDelta < 0 ? d.red : d.muted;
            var invSign = invDelta > 0 ? "+" : "";
            invHtml = " <span style=\"font-size:9px;color:" + invColor + ";font-weight:bold\">" + invSign + invDelta.toLocaleString("en-US") + " inv 24h</span>";
          }
          var bdRows = [
            { label: "Drop from weekly peak", pts: bd.drop || 0, max: 60 },
            { label: "Position in range",     pts: bd.position || 0, max: 35 },
            { label: "Short-term rise",       pts: bd.reversal || 0, max: 40 },
            { label: "MACD",                  pts: bd.macd || 0, max: 25 },
            { label: "RSI (Torn-calibrated)", pts: bd.rsi || 0, max: 20 },
          ];
          var bdHtml = bdRows.map(function(row) {
            var barW = row.max > 0 ? Math.round((row.pts / row.max) * 100) : 0;
            var barColor = row.pts >= row.max * 0.75 ? d.green : row.pts >= row.max * 0.4 ? d.yellow : d.muted;
            var maxBadge = row.pts === row.max ? " <span style=\"color:" + d.green + ";font-size:9px\">★ MAX</span>" : "";
            return "<div style=\"margin-bottom:6px\">" +
              "<div style=\"display:flex;justify-content:space-between;font-size:10px;margin-bottom:2px\">" +
              "<span style=\"color:" + d.muted + "\">" + row.label + "</span>" +
              "<span style=\"color:" + d.text + ";font-weight:bold;" + ms + "\">" + row.pts + " / " + row.max + "p" + maxBadge + "</span>" +
              "</div>" +
              "<div style=\"height:4px;background:" + d.border + ";border-radius:2px\">" +
              "<div style=\"height:4px;width:" + barW + "%;background:" + barColor + ";border-radius:2px\"></div>" +
              "</div></div>";
          }).join("");
          var signalColor = s.signal === "STRONG BUY" ? "#FFD700" : s.signal === "BUY" ? d.green : s.signal === "CONSIDER" ? "#FFA500" : d.muted;
          // Color based on ownership: unowned=green, owned swing=amber, owned benefit=blue
          var symColor = !s.owned ? d.green : s.has_benefit ? d.blue : d.yellow;
          var rowBg = !s.owned ? d.rowBuy : s.has_benefit ? d.rowBenefit : (isDark2 ? "rgba(255,193,7,0.07)" : "#fffbeb");
          var rowBorder = !s.owned ? d.rowBuyBorder : s.has_benefit ? d.rowBenefitBorder : (isDark2 ? "rgba(255,193,7,0.25)" : "#fde68a");

          // Trend arrow vs previous load
          var prevP = prevPrices[s.symbol];
          var trendHtml = "";
          if (prevP && prevP > 0 && s.p_live > 0) {
            var diff = ((s.p_live - prevP) / prevP * 100);
            if (Math.abs(diff) >= 0.01) {
              var trendCol = diff > 0 ? d.green : d.red;
              var trendArr = diff > 0 ? "↑" : "↓";
              trendHtml = "<span style=\"font-size:9px;color:" + trendCol + ";font-weight:bold;margin-left:4px\">" + trendArr + Math.abs(diff).toFixed(2) + "%</span>";
            }
          }
          var isPinned = tsaPinned.indexOf(s.symbol) >= 0;
          var pinBtnHtml = "<button class=\"tsa-pin-btn" + (isPinned ? " pinned" : "") + "\" data-sym=\"" + s.symbol + "\" title=\"" + (isPinned ? "Unpin" : "Pin to top") + "\">📌</button>";

          // BB width context — shows whether the signal is in a volatile or quiet period
          var bbCtx = calcBBWidth(s.symbol, cachedPriceHistory);
          var bbHtml = "";
          if (bbCtx && bbCtx.percentile !== null) {
            var bbBarW = Math.round(bbCtx.percentile);
            var bbColor = bbCtx.percentile <= 15 ? d.yellow :
                          bbCtx.percentile <= 65 ? d.muted :
                          bbCtx.percentile <= 85 ? d.green : d.red;
            bbHtml = "<div style=\"margin-bottom:6px;border-top:1px solid " + d.divider + ";padding-top:6px\">" +
              "<div style=\"display:flex;justify-content:space-between;font-size:10px;margin-bottom:2px\">" +
              "<span style=\"color:" + d.muted + "\">Volatility</span>" +
              "<span style=\"color:" + bbColor + ";font-weight:bold;" + ms + "\">" + bbCtx.label + " · " + bbBarW + "%</span>" +
              "</div>" +
              "<div style=\"height:4px;background:" + d.border + ";border-radius:2px\">" +
              "<div style=\"height:4px;width:" + bbBarW + "%;background:" + bbColor + ";border-radius:2px\"></div>" +
              "</div></div>";
          }

          html += "<div style=\"margin-bottom:5px\">" +
            "<div class=\"tsa-buy-row\" data-symbol=\"" + s.symbol + "\" data-breakdown=\"" + breakdownId + "\" style=\"display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-radius:8px;cursor:pointer;background:" + rowBg + ";border:1px solid " + rowBorder + "\">" +
            "<div style=\"display:flex;flex-direction:column;gap:2px\">" +
            "<span style=\"font-size:13px;font-weight:bold;color:" + symColor + ";" + ms + "\">" + s.symbol + trendHtml + " " + pinBtnHtml + "</span>" +
            "<span style=\"font-size:10px;color:" + d.muted + "\">" + s.reasons.split(" | ").slice(0,2).join(" · ") + invHtml + "</span>" +
            "</div><div style=\"display:flex;flex-direction:column;align-items:flex-end;gap:2px\">" +
            "<span style=\"font-size:14px;font-weight:bold;color:" + symColor + ";" + ms + "\">" + s.score + "</span>" +
            "<span style=\"font-size:9px;color:" + signalColor + ";font-weight:bold\">" + s.signal + "</span>" +
            "<span id=\"" + breakdownId + "-caret\" style=\"font-size:9px;color:" + d.muted + "\">▶</span>" +
            "</div></div>" +
            "<div id=\"" + breakdownId + "\" style=\"display:none;background:" + d.txBg + ";border:1px solid " + d.txBorder + ";border-top:none;border-radius:0 0 8px 8px;padding:10px 12px 8px;margin-top:-4px\">" +
            bdHtml +
            bbHtml +
            buildGapHtml(bd, s.score, s.signal) +
            "<div style=\"border-top:1px solid " + d.divider + ";margin-top:4px;padding-top:6px;display:flex;justify-content:space-between;align-items:center\">" +
            "<span style=\"font-size:10px;color:" + d.muted + "\">Total</span>" +
            "<span style=\"font-size:12px;font-weight:bold;color:" + symColor + ";" + ms + "\">" + s.score + "p · " + s.signal + "</span>" +
            "<button class=\"tsa-goto-chart\" data-symbol=\"" + s.symbol + "\" style=\"padding:5px 10px;border-radius:6px;border:1px solid " + d.border + ";background:" + d.bg2 + ";color:" + d.muted + ";font-size:10px;cursor:pointer;font-weight:bold;\">📈 Chart</button>" +
            "</div></div>" +
            "</div>";
        });
        html += "</div>";
      } else {
        html += "<div style=\"padding:10px 14px 6px;background:" + d.bg + "\">" +
          "<div style=\"font-size:10px;letter-spacing:0.12em;color:" + d.muted + ";text-transform:uppercase;margin-bottom:8px;font-weight:bold\">Buy signals</div>" +
          "<div style=\"color:" + d.muted + ";font-size:11px;padding:8px 0\">No signals right now</div></div>";
      }

      // WATCH section — owned stocks with CONSIDER score
      if (getShowWatch() && watchList.length > 0) {
        html += "<div style=\"padding:10px 14px 6px;background:" + d.bg + "\">" +
          "<div style=\"font-size:10px;letter-spacing:0.12em;color:" + d.muted + ";text-transform:uppercase;margin-bottom:8px;font-weight:bold\">Watch (" + watchList.length + ")</div>";
        watchList.forEach(function(s) {
          var watchBreakdownId = "tsa-watch-breakdown-" + s.symbol;
          var bd = s.scoreBreakdown || {};
          var bdRows = [
            { label: "Drop from weekly peak", pts: bd.drop || 0, max: 60 },
            { label: "Position in range",     pts: bd.position || 0, max: 35 },
            { label: "Short-term rise",       pts: bd.reversal || 0, max: 40 },
            { label: "MACD",                  pts: bd.macd || 0, max: 25 },
            { label: "RSI (Torn-calibrated)", pts: bd.rsi || 0, max: 20 },
          ];
          var bdHtml = bdRows.map(function(row) {
            var barW = row.max > 0 ? Math.round((row.pts / row.max) * 100) : 0;
            var barColor = row.pts >= row.max * 0.75 ? d.green : row.pts >= row.max * 0.4 ? d.yellow : d.muted;
            var maxBadge = row.pts === row.max ? " <span style=\"color:" + d.green + ";font-size:9px\">★ MAX</span>" : "";
            return "<div style=\"margin-bottom:6px\">" +
              "<div style=\"display:flex;justify-content:space-between;font-size:10px;margin-bottom:2px\">" +
              "<span style=\"color:" + d.muted + "\">" + row.label + "</span>" +
              "<span style=\"color:" + d.text + ";font-weight:bold;" + ms + "\">" + row.pts + " / " + row.max + "p" + maxBadge + "</span>" +
              "</div>" +
              "<div style=\"height:4px;background:" + d.border + ";border-radius:2px\">" +
              "<div style=\"height:4px;width:" + barW + "%;background:" + barColor + ";border-radius:2px\"></div>" +
              "</div></div>";
          }).join("");
          var watchSymColor = s.has_benefit ? d.blue : d.yellow;
          var watchRowBg = s.has_benefit ? d.rowBenefit : (isDark2 ? "rgba(255,193,7,0.07)" : "#fffbeb");
          var watchRowBorder = s.has_benefit ? d.rowBenefitBorder : (isDark2 ? "rgba(255,193,7,0.25)" : "#fde68a");

          // BB width context for Watch stocks
          var watchBbCtx = calcBBWidth(s.symbol, cachedPriceHistory);
          var watchBbHtml = "";
          if (watchBbCtx && watchBbCtx.percentile !== null) {
            var watchBbBarW = Math.round(watchBbCtx.percentile);
            var watchBbColor = watchBbCtx.percentile <= 15 ? d.yellow :
                               watchBbCtx.percentile <= 65 ? d.muted :
                               watchBbCtx.percentile <= 85 ? d.green : d.red;
            watchBbHtml = "<div style=\"margin-bottom:6px;border-top:1px solid " + d.divider + ";padding-top:6px\">" +
              "<div style=\"display:flex;justify-content:space-between;font-size:10px;margin-bottom:2px\">" +
              "<span style=\"color:" + d.muted + "\">Volatility</span>" +
              "<span style=\"color:" + watchBbColor + ";font-weight:bold;" + ms + "\">" + watchBbCtx.label + " · " + watchBbBarW + "%</span>" +
              "</div>" +
              "<div style=\"height:4px;background:" + d.border + ";border-radius:2px\">" +
              "<div style=\"height:4px;width:" + watchBbBarW + "%;background:" + watchBbColor + ";border-radius:2px\"></div>" +
              "</div></div>";
          }

          html += "<div style=\"margin-bottom:5px\">" +
            "<div class=\"tsa-watch-row\" data-symbol=\"" + s.symbol + "\" data-breakdown=\"" + watchBreakdownId + "\" style=\"display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-radius:8px;cursor:pointer;background:" + watchRowBg + ";border:1px solid " + watchRowBorder + "\">" +
            "<div style=\"display:flex;flex-direction:column;gap:2px\">" +
            "<span style=\"font-size:13px;font-weight:bold;color:" + watchSymColor + ";" + ms + "\">" + s.symbol + "</span>" +
            "<span style=\"font-size:10px;color:" + d.muted + "\">" + s.reasons.split(" | ").slice(0,2).join(" · ") + "</span>" +
            "</div><div style=\"display:flex;flex-direction:column;align-items:flex-end;gap:2px\">" +
            "<span style=\"font-size:14px;font-weight:bold;color:" + watchSymColor + ";" + ms + "\">" + s.score + "</span>" +
            "<span style=\"font-size:9px;color:" + watchSymColor + ";font-weight:bold\">CONSIDER</span>" +
            "<span id=\"" + watchBreakdownId + "-caret\" style=\"font-size:9px;color:" + d.muted + "\">▶</span>" +
            "</div></div>" +
            "<div id=\"" + watchBreakdownId + "\" style=\"display:none;background:" + d.txBg + ";border:1px solid " + d.txBorder + ";border-top:none;border-radius:0 0 8px 8px;padding:10px 12px 8px;margin-top:-4px\">" +
            bdHtml +
            watchBbHtml +
            buildGapHtml(bd, s.score, s.signal) +
            "<div style=\"border-top:1px solid " + d.divider + ";margin-top:4px;padding-top:6px;display:flex;justify-content:space-between;align-items:center\">" +
            "<span style=\"font-size:10px;color:" + d.muted + "\">Total</span>" +
            "<span style=\"font-size:12px;font-weight:bold;color:" + watchSymColor + ";" + ms + "\">" + s.score + "p · CONSIDER</span>" +
            "<button class=\"tsa-goto-chart\" data-symbol=\"" + s.symbol + "\" style=\"padding:5px 10px;border-radius:6px;border:1px solid " + d.border + ";background:" + d.bg2 + ";color:" + d.muted + ";font-size:10px;cursor:pointer;font-weight:bold;\">📈 Chart</button>" +
            "</div></div>" +
            "</div>";
        });
        html += "</div>";
      }

      var allOwned = stockResults.filter(function(s) { return s.owned; });

      var isBenefitCategory = function(checkSym) {
        var o = ownedMap[checkSym];
        if (!o) return false;
        return o.has_dividend && o.benefit_shares > 0;
      };

      var isSwingCategory = function(checkSym) {
        var o = ownedMap[checkSym];
        if (!o) return false;
        return o.swing_shares > 0;
      };

      var swingTrades = allOwned.filter(function(s) { return isSwingCategory(s.symbol); });
      swingTrades.sort(function(a, b) { return (b.netProfitPct || -Infinity) - (a.netProfitPct || -Infinity); });

      // Browser notifications for PROFIT / STOP LOSS signals on swing trades
      (function() {
        if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
        var notified;
        try { notified = JSON.parse(localStorage.getItem("tsa_notified_signals") || "{}"); } catch(e) { notified = {}; }
        // Remove stale entries for signals that are no longer active
        var activeKeys = {};
        swingTrades.forEach(function(s) { if (s.sellSignal) activeKeys[s.symbol + "|" + s.sellSignal] = true; });
        Object.keys(notified).forEach(function(k) { if (!activeKeys[k]) delete notified[k]; });
        // Notify for new signals not yet seen
        swingTrades.forEach(function(s) {
          if (!s.sellSignal) return;
          var key = s.symbol + "|" + s.sellSignal;
          if (notified[key]) return;
          notified[key] = Date.now();
          var pct = (s.netProfitPct !== undefined ? (s.netProfitPct >= 0 ? "+" : "") + s.netProfitPct.toFixed(2) + "%" : "");
          new Notification("Torn Stock Signal", {
            body: s.symbol + " — " + s.sellSignal + (pct ? " (" + pct + ")" : ""),
            icon: "https://www.torn.com/favicon.ico"
          });
        });
        lsSet("tsa_notified_signals", JSON.stringify(notified));
      })();

      var benefitBlocks = allOwned.filter(function(s) { return isBenefitCategory(s.symbol); });
      benefitBlocks.sort(function(a, b) { return (b.dividendProgress || 0) - (a.dividendProgress || 0); });

      var renderStockRow = function(s, category) {
        var owned = ownedMap[s.symbol];
        var isBenefit = category === "benefit";
        var displayShares = isBenefit
          ? ((owned && owned.benefit_shares) || s.shares)
          : ((owned && owned.swing_shares) > 0 ? owned.swing_shares : s.shares);

        // For mixed stocks in swing section: compute swing-specific avg price
        // Transactions are sorted newest-first; swing shares come from newest blocks
        var swingNetProfitPct = null;
        var swingAvgPrice = null;
        if (!isBenefit && owned && owned.benefit_shares > 0 && owned.swing_shares > 0 && s.p_live > 0 && s.transactions && s.transactions.length > 0) {
          var swRem = owned.swing_shares;
          var swCost = 0, swCount = 0;
          s.transactions.forEach(function(t) {
            if (swRem <= 0) return;
            var take = Math.min(t.shares || 0, swRem);
            var price = (t.bought_price && t.bought_price > 0) ? t.bought_price : (s.avg_price || 0);
            if (price > 0) { swCost += take * price; swCount += take; }
            swRem -= (t.shares || 0);
          });
          if (swCount > 0) {
            swingAvgPrice = swCost / swCount;
            swingNetProfitPct = (s.p_live * 0.999 - swingAvgPrice) / swingAvgPrice * 100;
          }
        }
        var displayNetProfitPct = swingNetProfitPct !== null ? swingNetProfitPct : s.netProfitPct;
        var displayValue = displayShares > 0 && s.p_live ? "$" + Math.round(displayShares * s.p_live).toLocaleString("en-US") : "";
        var sharesStr = displayShares > 0 ? displayShares.toLocaleString("en-US") + " shares" + (displayValue ? " · " + displayValue : "") : "?";
        var targetLine = "";
        if (!isBenefit && s.avg_price > 0) {
          var profitPct = getProfitTarget();
          var avgForTarget = swingAvgPrice !== null ? swingAvgPrice : s.avg_price;
          var targetPrice = avgForTarget * (1 + profitPct / 100);
          var currentPctStr = displayNetProfitPct !== null ? (displayNetProfitPct >= 0 ? "+" : "") + displayNetProfitPct.toFixed(2) + "%" : "";
          var currentPctColor = (displayNetProfitPct || 0) >= 0 ? d.green : d.red;
          targetLine = "<span style=\"font-size:10px;color:" + d.muted + "\">Avg $" + avgForTarget.toFixed(2) + " → Target <strong style=\"color:" + d.green + "\">$" + targetPrice.toFixed(2) + "</strong> (+" + profitPct.toFixed(1) + "%)" +
            (currentPctStr ? " · <strong style=\"color:" + currentPctColor + "\">" + currentPctStr + "</strong>" : "") +
            "</span><br>";
        }
        var pct = displayNetProfitPct !== null
          ? (displayNetProfitPct >= 0 ? "+" : "") + displayNetProfitPct.toFixed(2) + "%"
          : (s.avg_price > 0 && s.p_live > 0
            ? (((s.p_live * 0.999 - s.avg_price) / s.avg_price * 100) >= 0 ? "+" : "") + ((s.p_live * 0.999 - s.avg_price) / s.avg_price * 100).toFixed(2) + "%"
            : "");
        var effectiveProfitPct = displayNetProfitPct !== null ? displayNetProfitPct
          : (s.avg_price > 0 && s.p_live > 0 ? (s.p_live * 0.999 - s.avg_price) / s.avg_price * 100 : null);
        var isProfit = (effectiveProfitPct || 0) >= 0;
        var col = isBenefit
          ? (effectiveProfitPct !== null ? (isProfit ? d.green : d.red) : d.blue)
          : (isProfit ? d.green : d.red);
        var rowBgStr = isBenefit ? "background:" + d.rowBenefit + ";border:1px solid " + d.rowBenefitBorder
          : isProfit ? "background:" + d.rowBuy + ";border:1px solid " + d.rowBuyBorder
          : "background:" + d.rowSell + ";border:1px solid " + d.rowSellBorder;
        var detailId = "tsa-detail-" + s.symbol + "-" + category;
        var txHtml = "";
        if (s.transactions && s.transactions.length > 0) {
          var txSwingLeft = (!isBenefit && owned && owned.swing_shares > 0) ? owned.swing_shares : Infinity;
          s.transactions.forEach(function(t, idx) {
            var txDate = t.time_bought ? new Date(t.time_bought * 1000).toLocaleDateString("en-GB", {day:"2-digit",month:"2-digit",year:"2-digit"}) : "?";
            var txShares = (t.shares || 0).toLocaleString("en-US");
            var txPrice = t.bought_price ? "$" + t.bought_price.toFixed(2) : "?";
            var txCurrentVal = t.shares && s.p_live ? t.shares * s.p_live : 0;
            var txInvested = t.shares && t.bought_price ? t.shares * t.bought_price : 0;
            var txProfit = txCurrentVal - txInvested - txCurrentVal * 0.001;
            var txPct = txInvested > 0 ? ((txProfit / txInvested) * 100).toFixed(2) : "?";
            var txSign = txProfit >= 0 ? "+" : "";
            var txColD = txProfit >= 0 ? d.green : d.red;
            // In swing section: transactions beyond swing_shares are benefit blocks
            var isBenefitTx = !isBenefit && owned && owned.benefit_shares > 0 && txSwingLeft <= 0;
            txSwingLeft -= (t.shares || 0);
            var txLabel = isBenefitTx ? "Benefit block" : "Block " + (idx + 1);
            var txLabelColor = isBenefitTx ? d.blue : d.muted;
            txHtml += "<div " + (!isBenefitTx ? "id=\"tsa-swing-tx-" + s.symbol + "-" + idx + "\" class=\"tsa-swing-tx-row\" data-sym=\"" + s.symbol + "\" data-shares=\"" + (t.shares || 0) + "\" data-label=\"Block " + (idx + 1) + "\" data-state=\"0\" " : "") + "style=\"display:flex;justify-content:space-between;align-items:center;padding:6px 4px;border-bottom:1px solid " + d.divider + ";font-size:11px;" + (!isBenefitTx ? "cursor:pointer;border-radius:4px;" : "") + "\">" +
              "<div><span style=\"color:" + txLabelColor + ";" + ms + "\">" + txLabel + "</span><span style=\"color:" + d.muted + ";margin-left:6px\">" + txDate + "</span><br>" +
              "<span style=\"color:" + d.text + ";" + ms + "\">" + txShares + " @ " + txPrice + "</span></div>" +
              "<div style=\"text-align:right\">" +
              "<span style=\"font-weight:bold;color:" + txColD + ";font-size:12px;\">" + txSign + "$" + Math.abs(Math.round(txProfit)).toLocaleString("en-US") + "</span><br>" +
              "<span style=\"font-weight:bold;color:" + txColD + ";font-size:11px;\">" + txSign + txPct + "%</span></div></div>";
          });
        }

        var totalInvested = 0, totalCurrentVal = 0;
        if (s.transactions && s.transactions.length > 0) {
          s.transactions.forEach(function(t) {
            var costPrice = (t.bought_price && t.bought_price > 0) ? t.bought_price : (s.avg_price || 0);
            if (costPrice > 0) {
              totalInvested += (t.shares || 0) * costPrice;
              totalCurrentVal += (t.shares || 0) * (s.p_live || 0);
            }
          });
        }
        var totalProfit = totalCurrentVal - totalInvested - totalCurrentVal * 0.001;
        var totalPct = totalInvested > 0 ? ((totalProfit / totalInvested) * 100).toFixed(2) : null;
        var totalCol = (totalPct === null) ? d.muted : (totalProfit >= 0 ? d.green : d.red);
        var totalSign = totalProfit >= 0 ? "+" : "";

        return "<div style=\"margin-bottom:5px;\">" +
          "<div style=\"display:flex;align-items:center;gap:6px;\">" +
          "<div style=\"" + rowBgStr + ";flex:1;margin-bottom:0;cursor:pointer;display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-radius:8px;\" data-detail=\"" + detailId + "\" data-symbol=\"" + s.symbol + "\">" +
          "<div style=\"display:flex;flex-direction:column;gap:2px\">" +
          "<span style=\"font-size:13px;font-weight:bold;color:" + col + ";" + ms + "\">" + s.symbol + "</span>" +
          "<span style=\"font-size:10px;color:" + d.muted + "\">" + sharesStr + "</span><br>" +
          targetLine +
          "<span style=\"font-size:10px;color:" + d.muted + "\">Score " + s.score + (s.hasDividend ? " · DIV" : "") + (s.reasons ? " · " + s.reasons.split(" | ").slice(0,2).join(" · ") : "") + "</span>" +
          "</div><div style=\"display:flex;flex-direction:column;align-items:flex-end;gap:2px\">" +
          "<span style=\"font-size:13px;font-weight:bold;color:" + col + ";" + ms + "\">" + pct + "</span>" +
          (s.hasDividend ? "<span style=\"font-size:9px;padding:2px 6px;border-radius:10px;font-weight:bold;background:rgba(255,193,7,0.12);color:" + d.yellow + ";border:1px solid rgba(255,193,7,0.3)\">DIV " + s.dividendProgress + "/" + s.dividendFrequency + "d</span>" : "") +
          (s.sellSignal ? "<span style=\"font-size:9px;font-weight:bold;color:" + d.red + "\">" + s.sellSignal + "</span>" : "") +
          "</div></div>" +
          "</div>" +
          "<div id=\"" + detailId + "\" style=\"display:none;background:" + d.txBg + ";border:1px solid " + d.txBorder + ";border-radius:0 0 8px 8px;padding:8px 10px;margin-top:-4px;\">" +
          // Total P/L summary
          "<div style=\"display:flex;justify-content:space-between;align-items:center;background:" + d.bg + ";border-radius:6px;padding:8px 10px;margin-bottom:8px;border:1px solid " + d.border + "\">" +
          "<div><div style=\"font-size:9px;color:" + d.muted + ";text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px\">Total P/L</div>" +
          "<div style=\"font-size:15px;font-weight:bold;color:" + totalCol + "\">" + (totalPct !== null ? totalSign + "$" + Math.abs(Math.round(totalProfit)).toLocaleString("en-US") : "N/A") + "</div></div>" +
          "<div style=\"text-align:right\"><div style=\"font-size:9px;color:" + d.muted + ";text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px\">Return</div>" +
          "<div style=\"font-size:15px;font-weight:bold;color:" + totalCol + "\">" + (totalPct !== null ? totalSign + totalPct + "%" : "N/A") + "</div></div>" +
          "<button class=\"tsa-goto-chart\" data-symbol=\"" + s.symbol + "\" style=\"padding:6px 10px;border-radius:6px;border:1px solid " + d.border + ";background:" + d.bg2 + ";color:" + d.muted + ";font-size:10px;cursor:pointer;font-weight:bold;\">📈 Chart</button>" +
          "</div>" +
          (function() {
            // Entry slippage: compare QT buy intent price vs actual avg_price
            try {
              var intentRaw = localStorage.getItem("qt_intent_" + s.symbol);
              if (intentRaw && s.avg_price > 0) {
                var intent = JSON.parse(intentRaw);
                var ageSecs = Math.floor(Date.now() / 1000) - (intent.ts || 0);
                if (ageSecs < 86400 && intent.price > 0) {
                  var slipPct = (s.avg_price - intent.price) / intent.price * 100;
                  if (Math.abs(slipPct) >= 0.5) {
                    var slipColor = slipPct > 0 ? d.red : d.green;
                    var slipSign  = slipPct > 0 ? "+" : "";
                    return "<div style=\"display:flex;align-items:center;gap:6px;padding:5px 8px;margin-bottom:6px;border-radius:6px;background:" + (slipPct > 0 ? "rgba(255,76,106,0.08)" : "rgba(76,255,145,0.08)") + ";border:1px solid " + (slipPct > 0 ? "rgba(255,76,106,0.25)" : "rgba(76,255,145,0.25)") + "\">" +
                      "<span style=\"font-size:11px\">" + (slipPct > 0 ? "⚠" : "✓") + "</span>" +
                      "<span style=\"font-size:10px;color:" + d.muted + "\">Entry slippage: intended <strong style=\"color:" + d.text + "\">$" + intent.price.toFixed(2) + "</strong> · paid <strong style=\"color:" + slipColor + "\">$" + s.avg_price.toFixed(2) + "</strong> <strong style=\"color:" + slipColor + "\">(" + slipSign + slipPct.toFixed(1) + "%)</strong></span>" +
                      "</div>";
                  }
                }
              }
            } catch(e) {}
            return "";
          })() +
          "<div style=\"font-size:10px;color:" + d.muted + ";margin-bottom:4px;font-weight:bold;text-transform:uppercase;letter-spacing:0.08em;" + ms + "\">Transactions</div>" +
          "<div style=\"font-size:11px;color:" + d.muted + ";margin-bottom:6px;" + ms + "\">Avg: <strong style=\"color:" + d.text + "\">" + (s.avg_price > 0 ? "$" + s.avg_price.toFixed(2) : "N/A") + "</strong> · Live: <strong style=\"color:" + d.text + "\">$" + s.p_live.toFixed(2) + "</strong>" + (s.hoursHeld ? " · <span style=\"color:" + d.muted + "\">held " + s.hoursHeld + "h</span>" : "") + "</div>" +
          txHtml + "</div></div>";
      };

      if (allOwned.length > 0) {
        html += "<hr style=\"border:none;border-top:1px solid " + d.divider + ";margin:6px 14px\">";

        if (swingTrades.length > 0) {
          var swingCollapsed = lsGet("tsa_swing_collapsed", "false") === "true";
          html += "<div style=\"padding:10px 14px 6px;background:" + d.bg + "\">" +
            "<div style=\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;cursor:pointer;\" id=\"tsa-swing-header\">" +
            "<span style=\"font-size:10px;letter-spacing:0.12em;color:" + d.muted + ";text-transform:uppercase;font-weight:bold;" + ms + "\">Swing trades (" + swingTrades.length + ")</span>" +
            "<span style=\"font-size:14px;color:" + d.muted + ";\">" + (swingCollapsed ? "&#9658;" : "&#9660;") + "</span></div>" +
            "<div id=\"tsa-swing-body\" style=\"display:" + (swingCollapsed ? "none" : "block") + "\">";
          swingTrades.forEach(function(s) { html += renderStockRow(s, "swing"); });
          html += "</div></div>";
        }

        if (benefitBlocks.length > 0) {
          if (swingTrades.length > 0) html += "<hr style=\"border:none;border-top:1px solid " + d.divider + ";margin:6px 14px\">";
          var benefitCollapsed = lsGet("tsa_benefit_collapsed", "false") === "true";
          html += "<div style=\"padding:10px 14px 6px;background:" + d.bg + "\">" +
            "<div style=\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;cursor:pointer;\" id=\"tsa-benefit-header\">" +
            "<span style=\"font-size:10px;letter-spacing:0.12em;color:" + d.muted + ";text-transform:uppercase;font-weight:bold;" + ms + "\">Benefit blocks (" + benefitBlocks.length + ")</span>" +
            "<span style=\"font-size:14px;color:" + d.muted + ";\">" + (benefitCollapsed ? "&#9658;" : "&#9660;") + "</span></div>" +
            "<div id=\"tsa-benefit-body\" style=\"display:" + (benefitCollapsed ? "none" : "block") + "\">";
          benefitBlocks.forEach(function(s) { html += renderStockRow(s, "benefit"); });
          html += "</div></div>";
        }
      }

      var autoRefreshMins = getAutoRefreshInterval();
      var autoRefreshLabel = autoRefreshMins > 0 ? " · Auto " + autoRefreshMins + "m" : "";
      var histSyms = Object.keys(cachedPriceHistory);
      var maxHistHours = 0;
      histSyms.forEach(function(sym) {
        var entries = cachedPriceHistory[sym];
        if (!entries || entries.length < 2) return;
        var hrs = Math.round((entries[entries.length - 1].ts - entries[0].ts) / 3600000);
        if (hrs > maxHistHours) maxHistHours = hrs;
      });
      var histLabel = histSyms.length > 0 ? " · " + histSyms.length + " stocks · " + maxHistHours + "h hist" : "";
      html += "<div style=\"padding:10px 14px;display:flex;justify-content:space-between;align-items:center;border-top:1px solid " + d.divider + ";background:" + d.bg + "\">" +
        "<span style=\"font-size:10px;color:" + d.muted + "\">Updated: " + new Date().toLocaleTimeString("en-GB") + autoRefreshLabel + "<span id='tsa-countdown'></span></span>" +
        "<span style=\"font-size:10px;color:" + d.muted + "\">Storage: " + getTsaStorageSize() + histLabel + "</span>" +
        "</div>" +
        "<button id='tsa-scroll-top' title='Scroll to top'>↑</button>";
      scheduleAutoRefresh();

      content.innerHTML = html;

      // Buy signal chart click handler
      function drawChart(sym, chartId, stockData) {
        var chartDiv = document.getElementById(chartId);
        if (!chartDiv) return;
        var canvas = document.getElementById("canvas-" + sym);
        if (!canvas) return;

        var r = stockData ? stockData.find(function(x){ return x.stock === sym; }) : null;
        if (!r) { chartDiv.innerHTML += "<div style='font-size:9px;color:#888;text-align:center'>No data</div>"; return; }

        // Chronological order of spread prices
        var pts = [
          {l:"1W", p: parseFloat((r.interval && r.interval.w1 && r.interval.w1.price)) || 0},
          {l:"4D", p: parseFloat((r.interval && r.interval.d4 && r.interval.d4.price)) || 0},
          {l:"2D", p: parseFloat((r.interval && r.interval.d2 && r.interval.d2.price)) || 0},
          {l:"1D", p: parseFloat((r.interval && r.interval.d1 && r.interval.d1.price)) || 0},
          {l:"12H", p: parseFloat((r.interval && r.interval.h12 && r.interval.h12.price)) || 0},
          {l:"8H", p: parseFloat((r.interval && r.interval.h8 && r.interval.h8.price)) || 0},
          {l:"4H", p: parseFloat((r.interval && r.interval.h4 && r.interval.h4.price)) || 0},
          {l:"2H", p: parseFloat((r.interval && r.interval.h2 && r.interval.h2.price)) || 0},
          {l:"1H", p: parseFloat((r.interval && r.interval.h1 && r.interval.h1.price)) || 0},
          {l:"Now", p: parseFloat(r.price) || 0}
        ].filter(function(pt){ return pt.p > 0; });

        if (pts.length < 2) return;

        var isDarkC = document.getElementById("tsa-overlay").classList.contains("tsa-dark");
        var lineColor = isDarkC ? "#4cff91" : "#1a8a45";
        var gridColor = isDarkC ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)";
        var textColor = isDarkC ? "#7a7a9a" : "#888";

        var w = canvas.offsetWidth || 280;
        var h = 80;
        canvas.width = w;
        canvas.height = h;
        var ctx = canvas.getContext("2d");
        if (!ctx) return;

        var prices = pts.map(function(pt){ return pt.p; });
        var minP = Math.min.apply(null, prices);
        var maxP = Math.max.apply(null, prices);
        var range = maxP - minP || 1;
        var pad = 8;

        ctx.clearRect(0, 0, w, h);

        // Grid lines
        ctx.strokeStyle = gridColor;
        ctx.lineWidth = 1;
        [0.25, 0.5, 0.75].forEach(function(f) {
          var y = pad + (1-f) * (h - pad*2);
          ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
        });

        // Price line
        ctx.strokeStyle = lineColor;
        ctx.lineWidth = 2;
        ctx.lineJoin = "round";
        ctx.beginPath();
        pts.forEach(function(pt, i) {
          var x = pad + (i / (pts.length-1)) * (w - pad*2);
          var y = pad + (1 - (pt.p - minP) / range) * (h - pad*2);
          if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        });
        ctx.stroke();

        // Fill under line
        ctx.fillStyle = isDarkC ? "rgba(76,255,145,0.06)" : "rgba(26,138,69,0.06)";
        ctx.lineTo(pad + (pts.length-1)/(pts.length-1) * (w-pad*2), h-pad);
        ctx.lineTo(pad, h-pad);
        ctx.closePath();
        ctx.fill();

        // Last point dot
        var lastX = w - pad;
        var lastY = pad + (1 - (pts[pts.length-1].p - minP) / range) * (h - pad*2);
        ctx.beginPath();
        ctx.arc(lastX, lastY, 3, 0, Math.PI*2);
        ctx.fillStyle = lineColor;
        ctx.fill();

        // Min/Max labels
        ctx.fillStyle = textColor;
        ctx.font = "9px Arial";
        ctx.textAlign = "right";
        ctx.fillText("$" + maxP.toFixed(2), w-2, pad+8);
        ctx.fillText("$" + minP.toFixed(2), w-2, h-pad+1);
      }

      // Helper: set stock in Quick Trade dropdown
      function qtSetStock(sym) {
        var hidden = document.getElementById("qt-stock");
        var search = document.getElementById("qt-stock-search");
        if (hidden) hidden.value = sym;
        if (search) search.value = sym;
        lsSet("qt_last_stock", sym);
        qtDrawChart(sym);
        qtUpdateExec();
      }

      document.querySelectorAll(".tsa-watch-row").forEach(function(row) {
        row.addEventListener("click", function() {
          var sym = row.dataset.symbol;
          qtSetStock(sym);
          var bdId = row.dataset.breakdown;
          if (bdId) {
            var panel = document.getElementById(bdId);
            if (panel) {
              var open = panel.style.display !== "none";
              panel.style.display = open ? "none" : "block";
              var caret = document.getElementById(bdId + "-caret");
              if (caret) caret.textContent = open ? "▶" : "▼";
            }
          }
        });
      });

      document.querySelectorAll(".tsa-buy-row").forEach(function(row) {
        row.addEventListener("click", function() {
          var sym = row.dataset.symbol;
          qtSetStock(sym);

          // Toggle score breakdown panel
          var bdId = row.dataset.breakdown;
          if (bdId) {
            var panel = document.getElementById(bdId);
            if (panel) {
              var open = panel.style.display !== "none";
              panel.style.display = open ? "none" : "block";
              var caret = document.getElementById(bdId + "-caret");
              if (caret) caret.textContent = open ? "▶" : "▼";
            }
          }
        });
      });

      // Pin button — stop propagation so row click doesn't also fire
      document.querySelectorAll(".tsa-pin-btn").forEach(function(btn) {
        btn.addEventListener("click", function(e) {
          e.stopPropagation();
          var sym = btn.dataset.sym;
          var idx = tsaPinned.indexOf(sym);
          if (idx >= 0) { tsaPinned.splice(idx, 1); } else { tsaPinned.push(sym); }
          lsSet("tsa_pinned", JSON.stringify(tsaPinned));
          loadData();
        });
      });

      // Scroll-to-top button visibility
      var overlayEl = document.getElementById("tsa-overlay");
      var scrollTopBtn = document.getElementById("tsa-scroll-top");
      if (scrollTopBtn) {
        scrollTopBtn.style.display = "flex";
        scrollTopBtn.style.opacity = overlayEl && overlayEl.scrollTop > 80 ? "1" : "0";
        scrollTopBtn.style.transition = "opacity 0.2s";
        scrollTopBtn.addEventListener("click", function() {
          if (overlayEl) overlayEl.scrollTo({ top: 0, behavior: "smooth" });
        });
        if (overlayEl) {
          overlayEl.addEventListener("scroll", function() {
            if (!scrollTopBtn) return;
            scrollTopBtn.style.opacity = overlayEl.scrollTop > 80 ? "1" : "0";
          }, { passive: true });
        }
      }

      // Chart button in detail panel
      document.querySelectorAll(".tsa-goto-chart").forEach(function(btn) {
        btn.addEventListener("click", function(e) {
          e.stopPropagation();
          var sym = btn.dataset.symbol;
          if (!isDesktop) {
            var overlay = document.getElementById("tsa-overlay");
            if (overlay) overlay.style.display = "none";
          }
          var allImgs = document.querySelectorAll("img[src*='/logos/']");
          allImgs.forEach(function(img) {
            if (img.src.toLowerCase().indexOf("/" + sym.toLowerCase() + ".svg") >= 0) {
              var el = img;
              for (var i = 0; i < 8; i++) {
                el = el.parentElement;
                if (!el) break;
                if (el.className && el.className.indexOf("stock___") >= 0) {
                  el.scrollIntoView({ behavior: "smooth", block: "center" });
                  break;
                }
              }
            }
          });
        });
      });

      document.querySelectorAll("[data-detail]").forEach(function(row) {
        row.addEventListener("click", function() {
          if (row.dataset.symbol) qtSetStock(row.dataset.symbol);
          var panel = document.getElementById(row.dataset.detail);
          if (!panel) return;
          var opening = panel.style.display === "none";
          panel.style.display = opening ? "block" : "none";
          if (opening) {
            var overlay = document.getElementById("tsa-overlay");
            if (overlay) {
              var targetTop = row.offsetTop - 50; // 50px offset for sticky header
              overlay.scrollTo({ top: Math.max(0, targetTop), behavior: "smooth" });
            }
          }
        });
      });

      var realizedRow = document.getElementById("tsa-realized-row");
      if (realizedRow) realizedRow.addEventListener("click", function() {
        lsSet("tsa_realized_expanded", String(lsGet("tsa_realized_expanded", "false") !== "true"));
        loadData();
      });

      var swingHeader = document.getElementById("tsa-swing-header");
      if (swingHeader) swingHeader.addEventListener("click", function() {
        lsSet("tsa_swing_collapsed", String(lsGet("tsa_swing_collapsed", "false") !== "true"));
        loadData();
      });

      content.querySelectorAll(".tsa-swing-tx-row").forEach(function(row) {
        row.addEventListener("click", function(e) {
          e.stopPropagation();
          var sym    = row.dataset.sym;
          var shares = parseInt(row.dataset.shares, 10);
          var label  = row.dataset.label;
          var state  = row.dataset.state || "0";
          var isDarkSell = document.getElementById("tsa-overlay").classList.contains("tsa-dark");
          var sellColor  = isDarkSell ? "#ff4c6a" : "#cc2222";
          var sellBg     = isDarkSell ? "rgba(255,76,106,0.1)" : "rgba(204,34,34,0.08)";
          if (state === "0") {
            row.dataset.state = "1";
            row.dataset.origHtml = row.innerHTML;
            row.style.background = sellBg;
            row.style.borderLeft = "2px solid " + sellColor;
            row.style.paddingLeft = "8px";
            row.innerHTML =
              "<div style=\"display:flex;flex-direction:column;gap:2px\">" +
                "<span style=\"font-size:10px;font-weight:700;color:" + sellColor + ";font-family:JetBrains Mono,monospace\">▲ Tap again to confirm</span>" +
                "<span style=\"font-size:9px;color:" + sellColor + ";font-family:JetBrains Mono,monospace\">SELL " + shares.toLocaleString("en-US") + " " + sym + " · " + label + "</span>" +
              "</div>";
            setTimeout(function() {
              if (row.dataset.state === "1") {
                row.dataset.state = "0";
                row.style.background = "";
                row.style.borderLeft = "";
                row.style.paddingLeft = "";
                row.innerHTML = row.dataset.origHtml || "";
              }
            }, 4000);
          } else {
            row.dataset.state = "0";
            var parent = row.parentNode;
            if (parent) parent.removeChild(row);
            qtBuildMaps();
            qtPostTrade(sym, shares, "sellShares", "Sold " + shares.toLocaleString("en-US") + " " + sym + " (" + label + ")");
          }
        });
      });

      var benefitHeader = document.getElementById("tsa-benefit-header");
      if (benefitHeader) benefitHeader.addEventListener("click", function() {
        lsSet("tsa_benefit_collapsed", String(lsGet("tsa_benefit_collapsed", "false") !== "true"));
        loadData();
      });

    }).catch(function(e) {
      content.innerHTML = "<div class=\"tsa-error\">Error: " + escHtml(e.message) + "</div>" +
        "<div class=\"tsa-footer\"><span></span><button class=\"tsa-refresh\" id=\"tsa-refresh-btn\">Retry</button></div>";
      var retryBtn = document.getElementById("tsa-refresh-btn");
      if (retryBtn) retryBtn.addEventListener("click", loadData);
    });
  }

  // ── Signal history: logs buy signals with timestamp to localStorage ──
  var SIGNAL_HISTORY_KEY = "tsa_signal_history";
  var SIGNAL_HISTORY_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days

  function loadSignalHistory() {
    try { return JSON.parse(localStorage.getItem(SIGNAL_HISTORY_KEY)) || []; } catch(e) { return []; }
  }

  function saveSignalHistory(history) {
    try { localStorage.setItem(SIGNAL_HISTORY_KEY, JSON.stringify(history)); } catch(e) {}
  }

  function recordSignals(stockResults) {
    var now = Date.now();
    var nowMin = Math.round(now / 60000) * 60000;
    var cutoff = now - SIGNAL_HISTORY_MAX_AGE;
    var history = loadSignalHistory().filter(function(e) { return e.ts >= cutoff; });
    // Build set of sym+minute combos already logged
    var existing = {};
    history.forEach(function(e) {
      existing[e.sym + "_" + Math.round(e.ts / 60000)] = true;
    });
    stockResults.forEach(function(s) {
      if (s.signal === "STRONG BUY" || s.signal === "BUY") {
        var key = s.symbol + "_" + Math.round(nowMin / 60000);
        if (!existing[key]) {
          history.push({ ts: nowMin, sym: s.symbol, signal: s.signal, score: s.score, price: s.p_live });
          existing[key] = true;
        }
      }
    });
    saveSignalHistory(history);
  }

  // ── Price + investors history storage ──
  var HISTORY_KEY = "tsa_price_history";
  var INVESTOR_HISTORY_KEY = "tsa_investor_history";
  var HISTORY_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // default 30 days

  function getHistoryMaxAge() {
    var days = parseInt(lsGet("tsa_history_days", "30"), 10);
    if (isNaN(days) || days < 1) days = 1;
    if (days > 30) days = 30;
    return days * 24 * 60 * 60 * 1000;
  }

  function loadHistory() {
    try { return JSON.parse(localStorage.getItem(HISTORY_KEY)) || {}; } catch(e) { return {}; }
  }

  function saveHistory(history) {
    try {
      localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
    } catch(e) {
      // Quota exceeded — try pruning to progressively shorter windows before giving up
      var fallbackDays = [3, 1];
      var saved = false;
      for (var fi = 0; fi < fallbackDays.length; fi++) {
        var cutoff = Date.now() - fallbackDays[fi] * 24 * 60 * 60 * 1000;
        var reduced = {};
        Object.keys(history).forEach(function(sym) {
          var filtered = history[sym].filter(function(p) { return p.ts >= cutoff; });
          if (filtered.length > 0) reduced[sym] = filtered;
        });
        try {
          localStorage.setItem(HISTORY_KEY, JSON.stringify(reduced));
          saved = true;
          break;
        } catch(e2) {}
      }
      if (!saved && !saveHistory._warned) {
        saveHistory._warned = true;
        showToast("Storage full — price history could not be saved", "warn");
      }
    }
  }

  function loadInvestorHistory() {
    try { return JSON.parse(localStorage.getItem(INVESTOR_HISTORY_KEY)) || {}; } catch(e) { return {}; }
  }

  function saveInvestorHistory(history) {
    try { localStorage.setItem(INVESTOR_HISTORY_KEY, JSON.stringify(history)); } catch(e) {}
  }

  function pruneInvestorHistory(history) {
    var cutoff = Date.now() - getHistoryMaxAge();
    Object.keys(history).forEach(function(sym) {
      history[sym] = history[sym].filter(function(p) { return p.ts >= cutoff; });
      if (history[sym].length === 0) delete history[sym];
    });
    return history;
  }

  // Returns investor delta vs ~24h ago for a given symbol, or null if no data
  // Pass pre-loaded history to avoid repeated localStorage reads
  function getInvestorDelta(sym, cachedHistory) {
    var history = cachedHistory || loadInvestorHistory();
    var entries = history[sym.toUpperCase()];
    if (!entries || entries.length < 2) return null;
    var now = Date.now();
    var target = now - 24 * 3600 * 1000;
    var closest = null, closestDiff = Infinity;
    entries.forEach(function(e) {
      var diff = Math.abs(e.ts - target);
      if (diff < closestDiff) { closestDiff = diff; closest = e; }
    });
    if (!closest) return null;
    if (closest.ts > now - 30 * 60 * 1000) return null;
    var latest = entries[entries.length - 1];
    return latest.investors - closest.investors;
  }

  function pruneHistory(history) {
    var cutoff = Date.now() - getHistoryMaxAge();
    Object.keys(history).forEach(function(sym) {
      history[sym] = history[sym].filter(function(p) { return p.ts >= cutoff; });
      if (history[sym].length === 0) delete history[sym];
    });
    return history;
  }

  function recordPrices(raw) {
    var history = pruneHistory(loadHistory());
    var now = Date.now();
    var intervalMs = { "w1":7*24*3600*1000,"d7":7*24*3600*1000,"d6":6*24*3600*1000,"d5":5*24*3600*1000,"d4":4*24*3600*1000,"d3":3*24*3600*1000,"d2":2*24*3600*1000,"d1":24*3600*1000,"h20":20*3600*1000,"h16":16*3600*1000,"h12":12*3600*1000,"h10":10*3600*1000,"h8":8*3600*1000,"h6":6*3600*1000,"h5":5*3600*1000,"h4":4*3600*1000,"h3":3*3600*1000,"h2":2*3600*1000,"h1":3600*1000,"m30":30*60*1000 };
    var INTERVALS = Object.keys(intervalMs);

    raw.forEach(function(r) {
      var sym = r.stock;
      if (!sym) return;
      if (!history[sym]) history[sym] = [];

      // Build a map of existing ts values for fast dedup
      var existingTs = {};
      history[sym].forEach(function(p) { existingTs[p.ts] = true; });

      // Store all 20 intervals — skip if this exact ts already exists
      INTERVALS.forEach(function(key) {
        var p = parseFloat((r.interval && r.interval[key] && r.interval[key].price)) || 0;
        if (p <= 0) return;
        var ts = now - (intervalMs[key] || 0);
        // Round to nearest minute to allow dedup across sessions
        ts = Math.round(ts / 60000) * 60000;
        if (!existingTs[ts]) {
          history[sym].push({ ts: ts, price: p });
          existingTs[ts] = true;
        }
      });

      // Store live price — always overwrite if same-minute ts exists
      var p_live = parseFloat(r.price) || 0;
      if (p_live > 0) {
        var liveTs = Math.round(now / 60000) * 60000;
        // Remove old entry for this ts if exists, then add fresh
        history[sym] = history[sym].filter(function(p) { return p.ts !== liveTs; });
        history[sym].push({ ts: liveTs, price: p_live });
      }

      // Sort by time
      history[sym].sort(function(a, b) { return a.ts - b.ts; });
    });

    saveHistory(history);

    // Record investor counts
    var invHistory = pruneInvestorHistory(loadInvestorHistory());
    var nowMin = Math.round(now / 60000) * 60000;
    raw.forEach(function(r) {
      var sym = r.stock;
      if (!sym || !r.investors) return;
      var investors = parseInt(r.investors, 10) || 0;
      if (investors <= 0) return;
      if (!invHistory[sym]) invHistory[sym] = [];
      // Dedup by minute
      var existingTs = {};
      invHistory[sym].forEach(function(e) { existingTs[e.ts] = true; });
      if (!existingTs[nowMin]) {
        invHistory[sym].push({ ts: nowMin, investors: investors });
        invHistory[sym].sort(function(a, b) { return a.ts - b.ts; });
      }
    });
    saveInvestorHistory(invHistory);

    // Return history so caller can reuse without a second localStorage parse
    return history;
  }

  // Track recommendation tap state per symbol
  var qtRecTapState = {};

  function updateQtRecommendation(sym) {
    var recDiv = document.getElementById("qt-rec");
    if (!recDiv) {
      // Bar not ready yet — retry after delay
      setTimeout(function() { updateQtRecommendation(sym); }, 500);
      return;
    }
    if (!lastBestRec) { recDiv.style.display = "none"; return; }

    var rec = lastBestRec;
    var cash = typeof qtGetMoneyFast === "function" ? qtGetMoneyFast() : 0;
    var canAfford = cash >= rec.cost;
    var color = canAfford ? "#4cff91" : "#ff4c6a";
    var border = canAfford ? "rgba(76,255,145,0.3)" : "rgba(255,76,106,0.3)";
    var bg = canAfford ? "rgba(76,255,145,0.07)" : "rgba(255,76,106,0.06)";
    var recSym = rec.sym;
    var recTier = "T" + rec.tierInfo.nextIncrement;
    var recShares = rec.tierInfo.sharesNeeded;
    var recCost = rec.cost;

    var tapState = qtRecTapState[recSym + recTier] || 0;
    var label = tapState === 0
      ? "💡 " + recSym + " " + recTier + " · " + recShares.toLocaleString("en-US") + " shares · " + fmRoi(recCost)
      : "▲ Buy " + recShares.toLocaleString("en-US") + " " + recSym + " — tap to confirm";

    recDiv.style.display = "block";
    recDiv.innerHTML = "<button id='qt-rec-btn' style='width:100%;padding:6px 10px;border-radius:7px;border:1px solid " + border + ";background:" + bg + ";color:" + color + ";font-family:JetBrains Mono,monospace;font-size:11px;font-weight:700;cursor:pointer;text-align:left;'>" + label + "</button>";

    document.getElementById("qt-rec-btn").addEventListener("click", function() {
      var state = qtRecTapState[recSym + recTier] || 0;
      if (state === 0) {
        // Press 1: Scroll to stock
        qtBuildMaps();
        var allImgs = document.querySelectorAll("img[src*='/logos/']");
        allImgs.forEach(function(img) {
          if (img.src.toLowerCase().indexOf("/" + recSym.toLowerCase() + ".svg") >= 0) {
            var el = img;
            for (var i = 0; i < 8; i++) {
              el = el.parentElement;
              if (!el) break;
              if (el.className && el.className.indexOf("stock___") >= 0) {
                el.scrollIntoView({ behavior: "smooth", block: "center" });
                break;
              }
            }
          }
        });
        qtRecTapState[recSym + recTier] = 1;
        updateQtRecommendation(null);
      } else {
        // Press 2: Buy
        qtBuildMaps();
        qtPostTrade(recSym, recShares, "buyShares", "Bought " + recShares.toLocaleString("en-US") + " " + recSym + " (" + recTier + ")");
        qtRecTapState[recSym + recTier] = 0;
        updateQtRecommendation(null);
      }
    });
  }
  function getRoiRecommendation(sym) {
    if (!lastOwnedMap || !lastRaw) return null;
    var tierInfo = calcNextTier(sym, lastOwnedMap, lastRaw);
    if (!tierInfo || tierInfo.sharesNeeded <= 0) return null;
    // Check capital
    var liveEntry = lastRaw.find(function(x) { return x.stock === sym.toUpperCase(); });
    if (!liveEntry) return null;
    // Estimate available capital from cash (rough — exact is in ROI planner)
    return {
      sym: sym,
      sharesNeeded: tierInfo.sharesNeeded,
      cost: tierInfo.cost,
      tier: "T" + tierInfo.nextIncrement,
      livePrice: tierInfo.livePrice
    };
  }

  var QT_DEFAULT_AMOUNTS = [1000000, 3000000, 5000000, 10000000, 25000000, 50000000, 100000000];
  var qtAmounts = (function() {
    try {
      var stored = localStorage.getItem("qt_amounts");
      var parsed = stored ? JSON.parse(stored) : null;
      return (Array.isArray(parsed) && parsed.length > 0) ? parsed : QT_DEFAULT_AMOUNTS.slice();
    } catch(e) { return QT_DEFAULT_AMOUNTS.slice(); }
  })();
  var qtMode = "buy";
  var qtSelAmt = null;
  var qtEditMode = false;

  var QT_STOCKS = ["ASS","BAG","CBD","CNC","ELT","EVL","EWM","FHG","GRN","HRG",
    "IIL","IOU","IST","LAG","LOS","LSC","MCS","MSG","MUN","PRN",
    "PTS","SYM","SYS","TCC","TCI","TCM","TCP","TCT","TGP","THS",
    "TMI","TSB","WLT","WSU","YAZ"];

  // ── TheALFA's exact variables (from his insert()) ──
  var qt_stocks = {}, qt_stockId = {}, qt_stockRows = {}, qt_localShareCache = {};

  // ── TheALFA's exact insert() DOM parsing ──
  function qtBuildMaps() {
    $("ul[class^='stock_']").each(function() {
      var sym = $("img", $(this)).attr("src").split("logos/")[1].split(".svg")[0];
      qt_stockId[sym] = $(this).attr("id");
      qt_stocks[sym] = $("div[class^='price_']", $(this));
      qt_stockRows[sym] = $(this);
    });
  }

  // ── TheALFA's exact functions ──
  function qtGetRFC() { var c = document.cookie.match(/rfc_v=([^;]+)/); return c ? c[1] : ""; }

  function qtParseTornNumber(val) {
    if (typeof val !== "string") return 0;
    val = val.trim().toLowerCase();
    if (!val) return 0;
    var n;
    if (val.endsWith("k")) { n = parseFloat(val.replace("k", "")); return isNaN(n) ? 0 : n * 1000; }
    if (val.endsWith("m")) { n = parseFloat(val.replace("m", "")); return isNaN(n) ? 0 : n * 1000000; }
    if (val.endsWith("b")) { n = parseFloat(val.replace("b", "")); return isNaN(n) ? 0 : n * 1000000000; }
    n = parseFloat(val.replace(/,/g, ""));
    return isNaN(n) ? 0 : n;
  }

  function qtGetPrice(id) {
    if (!qt_stocks[id]) return 0;
    return parseFloat($(qt_stocks[id]).text().replace(/,/g, "")) || 0;
  }

  function qtGetOwnedShares(id) {
    if (qt_localShareCache[id] !== undefined) return qt_localShareCache[id];
    var row = qt_stockRows[id];
    if (!row) return 0;
    var mobileEl = row.find("p[class^='count']");
    if (mobileEl.length > 0) return parseFloat(mobileEl.text().replace(/,/g, "")) || 0;
    var cols = row.children("div");
    if (cols.length >= 5) return parseFloat($(cols[4]).text().replace(/,/g, "")) || 0;
    return 0;
  }

  function qtUpdateLocalCache(sym, amt) {
    var current = qtGetOwnedShares(sym);
    qt_localShareCache[sym] = Math.max(0, current + amt);
  }

  function qtGetMoneyFast() {
    var dataMoney = $("#user-money").attr("data-money");
    if (dataMoney) return parseFloat(dataMoney);
    var textMoney = $("#user-money").text();
    return textMoney ? qtParseTornNumber(textMoney) : 0;
  }

  // Exact copy of TheALFA's getBenefitTier()
  function qtGetBenefitTier(sym, shares) {
    var data = BENEFIT_REQ[sym];
    if (!data) return { tier: 0, next: 0 };
    // TheALFA uses STOCK_DATA which has type "P" for passive stocks
    // Our BENEFIT_REQ only stores the base number, so we check our own map
    if (PASSIVE_STOCKS.indexOf(sym) >= 0) {
      return (shares >= data) ? { tier: 1, next: data } : { tier: 0, next: data };
    }
    var multiplier = 1;
    while (shares >= data * (multiplier * 2)) { multiplier *= 2; }
    return (shares < data) ? { tier: 0, next: data } : { tier: multiplier, next: data * multiplier * 2 };
  }

  // ── TheALFA's exact postTrade() ──
  function qtPostTrade(symb, amt, step, msg) {
    // Record buy intent so we can detect entry slippage after the next data load
    if (step === "buyShares") {
      var intentPrice = qtGetPrice(symb);
      if (intentPrice > 0) {
        lsSet("qt_intent_" + symb, JSON.stringify({ price: intentPrice, ts: Math.floor(Date.now() / 1000) }));
      }
    }
    var execBtn = document.getElementById("qt-exec");
    if (execBtn) { execBtn.disabled = true; execBtn.textContent = "Processing..."; }
    $.post("https://www.torn.com/page.php?sid=StockMarket&step=" + step + "&rfcv=" + qtGetRFC(),
      { stockId: qt_stockId[symb], amount: amt })
    .done(function(r) {
      try {
        if (typeof r === "string") r = JSON.parse(r);
        if (r.success) {
          if (execBtn) { execBtn.textContent = "✓ " + msg; setTimeout(function(){ qtUpdateExec(); }, 3000); }
          qtUpdateLocalCache(symb, step === "buyShares" ? amt : -amt);
          showToast(msg, "success");
        } else {
          showToast(r.text || "Trade failed", "error");
          if (execBtn) { execBtn.disabled = false; qtUpdateExec(); }
        }
      } catch(e) {
        if (execBtn) { execBtn.textContent = "✓ " + msg; setTimeout(function(){ qtUpdateExec(); }, 3000); }
      }
    })
    .fail(function() {
      showToast("Request failed", "error");
      if (execBtn) { execBtn.disabled = false; qtUpdateExec(); }
    });
  }

  // ── TheALFA's exact vault() ──
  function qtVault(symb) {
    var money = qtGetMoneyFast();
    if (money === 0) { showToast("No money found", "warn"); return; }
    var price = qtGetPrice(symb);
    var amt = Math.floor(money / price);
    if (amt <= 0) { showToast("Amount too small", "warn"); return; }
    qtPostTrade(symb, amt, "buyShares", "Vaulted $" + (amt*price).toLocaleString() + " (" + amt + " shares)");
  }

  // ── TheALFA's exact withdraw() ──
  // ── Shared benefit lock check — used by all three sell paths ──
  // Returns max shares that can be safely sold (Infinity = no restriction).
  // Blocks by returning 0 when all shares are locked or count is unverifiable.
  function qtBenefitLockMax(symb) {
    var currentOwned = qtGetOwnedShares(symb);
    var oe = lastOwnedMap ? lastOwnedMap[symb.toUpperCase()] : null;
    if (oe && oe.benefit_shares > 0) {
      // Precise path: live count minus static benefit_shares (benefit_shares never
      // decreases from selling, only from losing a tier which requires buying first)
      return Math.max(0, currentOwned - oe.benefit_shares);
    }
    if (BENEFIT_REQ[symb]) {
      // Fallback: tier-based check (TSA panel not yet loaded)
      if (currentOwned === 0) return -1; // sentinel: cannot verify → block
      var curTier = qtGetBenefitTier(symb, currentOwned);
      if (curTier.tier === 0) return Infinity; // no benefit tier → no restriction
      // Max sellable = current owned minus shares needed to stay at current tier
      var keepShares = PASSIVE_STOCKS.indexOf(symb) >= 0
        ? BENEFIT_REQ[symb]
        : BENEFIT_REQ[symb] * (2 * curTier.tier - 1);
      return Math.max(0, currentOwned - keepShares);
    }
    return Infinity; // not a benefit stock
  }

  function qtWithdraw(symb, val) {
    var price = qtGetPrice(symb);
    if (price <= 0) { showToast("Could not read price for " + symb, "error"); return; }
    var shares = Math.ceil((val / 0.999) / price);
    if ($("#qt-lock-benefit").is(":checked")) {
      var maxSell = qtBenefitLockMax(symb);
      if (maxSell === -1) { showToast("Benefit Lock: Cannot verify share count — blocked for safety", "warn"); return; }
      if (maxSell === 0)  { showToast("Benefit Lock: All shares are benefit block shares — cannot sell", "warn"); return; }
      if (maxSell !== Infinity && shares > maxSell) {
        shares = maxSell;
        showToast("Benefit Lock: Capped to " + maxSell.toLocaleString() + " swing shares", "warn");
      }
    }
    qtPostTrade(symb, shares, "sellShares", "Withdrawn approx $" + val.toLocaleString());
  }

  // ── TheALFA's exact withdrawAll() ──
  function qtWithdrawAll(symb) {
    var owned = qtGetOwnedShares(symb);
    if (owned <= 0) { showToast("You have no shares of " + symb, "warn"); return; }
    var sellAmt = owned;
    if ($("#qt-lock-benefit").is(":checked")) {
      var maxSell = qtBenefitLockMax(symb);
      if (maxSell === -1) { showToast("Benefit Lock: Cannot verify share count — blocked for safety", "warn"); return; }
      if (maxSell === 0)  { showToast("Benefit Lock: All shares are benefit block shares — cannot sell", "warn"); return; }
      if (maxSell !== Infinity) sellAmt = Math.min(sellAmt, maxSell);
    }
    qtPostTrade(symb, sellAmt, "sellShares", "Sold all " + sellAmt.toLocaleString() + " shares");
  }

  function fmtQtAmt(n) {
    n = Math.abs(n || 0);
    if (n >= 1e9) return (n/1e9 % 1 === 0 ? n/1e9 : (n/1e9).toFixed(1)) + "B";
    if (n >= 1e6) return (n/1e6 % 1 === 0 ? n/1e6 : (n/1e6).toFixed(1)) + "M";
    if (n >= 1e3) return Math.round(n/1e3) + "K";
    return "$" + n;
  }

  function saveQtAmounts() { lsSet("qt_amounts", JSON.stringify(qtAmounts)); }

  function renderQtAmounts() {
    var grid = document.getElementById("qt-amounts");
    if (!grid) return;
    grid.innerHTML = "";
    qtAmounts.forEach(function(amt, i) {
      var btn = document.createElement("button");
      var isActive = qtSelAmt === amt;
      btn.style.cssText = "position:relative;padding:6px 9px;border-radius:7px;border:1px solid " +
        (isActive ? (qtMode === "buy" ? "#4cff91" : "#ff4c6a") : "#2a2a4a") +
        ";background:" + (isActive ? (qtMode === "buy" ? "rgba(76,255,145,0.08)" : "rgba(255,76,106,0.08)") : "#13131f") +
        ";color:" + (isActive ? (qtMode === "buy" ? "#4cff91" : "#ff4c6a") : "#5a5a8a") +
        ";font-family:JetBrains Mono,monospace;font-size:11px;font-weight:700;cursor:pointer;white-space:nowrap;flex-shrink:0;";
      btn.textContent = fmtQtAmt(amt);
      if (qtEditMode) {
        var del = document.createElement("span");
        del.textContent = "✕";
        del.style.cssText = "position:absolute;top:-6px;right:-6px;width:16px;height:16px;border-radius:50%;background:#ff4c6a;color:#fff;font-size:9px;line-height:16px;text-align:center;cursor:pointer;";
        del.onclick = function(e) {
          e.stopPropagation();
          qtAmounts.splice(i, 1);
          if (qtSelAmt === amt) qtSelAmt = null;
          saveQtAmounts(); renderQtAmounts(); qtUpdateExec();
        };
        btn.appendChild(del);
      }
      btn.onclick = function() {
        if (qtEditMode) return;
        qtSelAmt = (qtSelAmt === amt) ? null : amt;
        renderQtAmounts(); qtUpdateExec();
      };
      grid.appendChild(btn);
    });
    if (qtEditMode) {
      var addBtn = document.createElement("button");
      addBtn.textContent = "+";
      addBtn.style.cssText = "padding:6px 10px;border-radius:7px;border:1px dashed #2a2a4a;background:transparent;color:#6a6a9a;font-family:JetBrains Mono,monospace;font-size:14px;cursor:pointer;flex-shrink:0;";
      addBtn.onclick = function() {
        var val = prompt("Enter amount in $ (e.g. 25000000):");
        val = parseInt(val, 10);
        if (val > 0) { qtAmounts.push(val); qtAmounts.sort(function(a,b){return a-b;}); saveQtAmounts(); renderQtAmounts(); }
      };
      grid.appendChild(addBtn);
    }
  }

  function qtUpdateExec() {
    var btn = document.getElementById("qt-exec");
    var stock = document.getElementById("qt-stock") ? document.getElementById("qt-stock").value : "";
    if (!btn) return;
    btn.disabled = false;
    btn.style.background = qtMode === "buy" ? "rgba(76,255,145,0.15)" : "rgba(255,76,106,0.12)";
    btn.style.color = qtMode === "buy" ? "#4cff91" : "#ff4c6a";
    btn.style.borderColor = qtMode === "buy" ? "rgba(76,255,145,0.25)" : "rgba(255,76,106,0.25)";
    btn.style.opacity = (!stock || !qtSelAmt) ? "0.4" : "1";
    btn.textContent = (qtMode === "buy" ? "▲ Buy " : "▼ Sell ") +
      (qtSelAmt ? fmtQtAmt(qtSelAmt) : "?") +
      (stock ? " of " + stock : " — select stock");
    // Swing shares available label (only when lock is on + stock is a benefit stock)
    var swingLabel = document.getElementById("qt-swing-available");
    var swingInfo  = document.getElementById("qt-benefit-info");
    if (swingLabel && swingInfo) {
      var lockOn = document.getElementById("qt-lock-benefit") && document.getElementById("qt-lock-benefit").checked;
      var maxSellDisp = (lockOn && stock) ? qtBenefitLockMax(stock) : Infinity;
      if (lockOn && stock && maxSellDisp !== Infinity) {
        var dispText = maxSellDisp === -1 ? "Cannot read share count"
                     : maxSellDisp === 0  ? "No swing shares — fully locked"
                     : maxSellDisp.toLocaleString("en-US") + " swing shares available to sell";
        swingLabel.textContent = "🔒 " + dispText;
        swingInfo.style.display = "block";
      } else {
        swingInfo.style.display = "none";
      }
    }
  }

  function qtExecuteBuy(sym, dollarAmt) {
    qtBuildMaps();
    if (!sym) { showToast("Select a stock first.", "warn"); return; }
    var price = qtGetPrice(sym);
    if (price <= 0) { showToast("Could not read price for " + sym, "error"); return; }
    var amt = Math.floor(dollarAmt / price);
    if (amt <= 0) { showToast("Amount too small.", "warn"); return; }
    qtPostTrade(sym, amt, "buyShares", "Bought " + amt.toLocaleString() + " " + sym);
  }

  function qtExecuteSell(sym, dollarAmt) {
    qtBuildMaps();
    if (!sym) { showToast("Select a stock first.", "warn"); return; }
    var price = qtGetPrice(sym);
    if (price <= 0) { showToast("Could not read price for " + sym, "error"); return; }
    var shares = Math.ceil((dollarAmt / 0.999) / price);
    if ($("#qt-lock-benefit").is(":checked")) {
      var maxSell = qtBenefitLockMax(sym);
      if (maxSell === -1) { showToast("Benefit Lock: Cannot verify share count — blocked for safety", "warn"); return; }
      if (maxSell === 0)  { showToast("Benefit Lock: All shares are benefit block shares — cannot sell", "warn"); return; }
      if (maxSell !== Infinity && shares > maxSell) {
        shares = maxSell;
        showToast("Benefit Lock: Capped to " + maxSell.toLocaleString() + " swing shares", "warn");
      }
    }
    qtPostTrade(sym, shares, "sellShares", "Sold " + shares.toLocaleString() + " " + sym);
  }

  function createAmountBtn(label, amt, mode, idx) {
    var btn = document.createElement("button");
    var isBuy = mode === "buy";
    var isDark = lsGet("tsa_dark", "false") === "true";
    var buyBorder  = isDark ? "rgba(76,255,145,0.3)"  : "#1a8a45";
    var buyBg      = isDark ? "rgba(76,255,145,0.08)" : "rgba(26,138,69,0.08)";
    var buyColor   = isDark ? "#4cff91"               : "#1a8a45";
    var sellBorder = isDark ? "rgba(255,76,106,0.3)"  : "#cc2222";
    var sellBg     = isDark ? "rgba(255,76,106,0.08)" : "rgba(204,34,34,0.08)";
    var sellColor  = isDark ? "#ff4c6a"               : "#cc2222";
    btn.style.cssText = "position:relative;padding:6px 9px;border-radius:7px;border:1px solid " +
      (isBuy ? buyBorder : sellBorder) +
      ";background:" + (isBuy ? buyBg : sellBg) +
      ";color:" + (isBuy ? buyColor : sellColor) +
      ";font-family:JetBrains Mono,monospace;font-size:11px;font-weight:700;cursor:pointer;white-space:nowrap;flex-shrink:0;";
    btn.textContent = label;

    if (qtEditMode) {
      // Del button
      var del = document.createElement("span");
      del.textContent = "✕";
      del.style.cssText = "position:absolute;top:-6px;right:-6px;width:16px;height:16px;border-radius:50%;background:#ff4c6a;color:#fff;font-size:9px;line-height:16px;text-align:center;cursor:pointer;";
      del.onclick = function(e) {
        e.stopPropagation();
        qtAmounts.splice(idx, 1);
        saveQtAmounts(); renderQtRows();
      };
      btn.appendChild(del);
      // Click to edit value
      btn.onclick = function() {
        var newVal = prompt("Edit amount:", amt);
        newVal = parseInt(newVal, 10);
        if (newVal > 0) {
          qtAmounts[idx] = newVal;
          qtAmounts.sort(function(a,b){return a-b;});
          saveQtAmounts(); renderQtRows();
        }
      };
    } else {
      btn.onclick = function() {
        var s = $("#qt-stock").val();
        if (!s) { showToast("Select a stock first.", "warn"); return; }
        if (isBuy) qtExecuteBuy(s, amt);
        else qtExecuteSell(s, amt);
      };
    }
    return btn;
  }

  function renderQtRows() {
    var buyRow = document.getElementById("qt-buy-row");
    var sellRow = document.getElementById("qt-sell-row");
    if (!buyRow || !sellRow) return;
    buyRow.innerHTML = "";
    sellRow.innerHTML = "";

    qtAmounts.forEach(function(amt, idx) {
      buyRow.appendChild(createAmountBtn(fmtQtAmt(amt), amt, "buy", idx));
      sellRow.appendChild(createAmountBtn(fmtQtAmt(amt), amt, "sell", idx));
    });

    // ALL buy btn
    var allBuyBtn = document.createElement("button");
    var isDarkNow = lsGet("tsa_dark", "false") === "true";
    allBuyBtn.style.cssText = "padding:6px 9px;border-radius:7px;border:1px solid " + (isDarkNow ? "rgba(76,255,145,0.5)" : "#1a8a45") + ";background:" + (isDarkNow ? "rgba(76,255,145,0.15)" : "rgba(26,138,69,0.1)") + ";color:" + (isDarkNow ? "#4cff91" : "#1a8a45") + ";font-family:JetBrains Mono,monospace;font-size:11px;font-weight:700;cursor:pointer;white-space:nowrap;flex-shrink:0;";
    allBuyBtn.textContent = "ALL";
    allBuyBtn.onclick = function() {
      var s = $("#qt-stock").val();
      if (!s) { showToast("Select a stock first.", "warn"); return; }
      qtBuildMaps(); qtVault(s);
    };
    buyRow.appendChild(allBuyBtn);

    // ALL sell btn
    var allSellBtn = document.createElement("button");
    allSellBtn.style.cssText = "padding:6px 9px;border-radius:7px;border:1px solid " + (isDarkNow ? "rgba(255,76,106,0.5)" : "#cc2222") + ";background:" + (isDarkNow ? "rgba(255,76,106,0.12)" : "rgba(204,34,34,0.08)") + ";color:" + (isDarkNow ? "#ff4c6a" : "#cc2222") + ";font-family:JetBrains Mono,monospace;font-size:11px;font-weight:700;cursor:pointer;white-space:nowrap;flex-shrink:0;";
    allSellBtn.textContent = "ALL";
    allSellBtn.onclick = function() {
      var s = $("#qt-stock").val();
      if (!s) { showToast("Select a stock first.", "warn"); return; }
      qtBuildMaps(); qtWithdrawAll(s);
    };
    sellRow.appendChild(allSellBtn);

    if (qtEditMode) {
      var addBtn = document.createElement("button");
      addBtn.style.cssText = "padding:6px 10px;border-radius:7px;border:1px dashed #2a2a4a;background:transparent;color:#4a6fa5;font-family:JetBrains Mono,monospace;font-size:14px;cursor:pointer;flex-shrink:0;";
      addBtn.textContent = "+";
      addBtn.onclick = function() {
        var val = prompt("Enter amount in $ (e.g. 25000000):");
        val = parseInt(val, 10);
        if (val > 0) { qtAmounts.push(val); qtAmounts.sort(function(a,b){return a-b;}); saveQtAmounts(); renderQtRows(); }
      };
      buyRow.appendChild(addBtn);
    }
  }

  function qtDrawChart(sym) {
    var container = document.getElementById("qt-chart-container");
    var canvas = document.getElementById("qt-chart-canvas");
    var title = document.getElementById("qt-chart-title");
    var liveEl = document.getElementById("qt-chart-live");
    var labelsEl = document.getElementById("qt-chart-labels");
    if (!container || !canvas) return;

    if (!sym) { container.style.display = "none"; return; }

    var p_live = 0;
    if (lastRaw) {
      var r = lastRaw.find(function(x) { return x.stock === sym.toUpperCase(); });
      if (r) p_live = parseFloat(r.price) || 0;
    }

    // Load ALL stored history for this stock
    var history = loadHistory();
    var points = (history[sym.toUpperCase()] || []).map(function(h) {
      return { ts: h.ts, price: h.price };
    });

    // Add live price as final point if it differs from last stored
    var now = Date.now();
    if (p_live > 0) {
      var liveTs = Math.round(now / 60000) * 60000;
      // Remove any duplicate ts at this minute, then push fresh live
      points = points.filter(function(p) { return p.ts !== liveTs; });
      points.push({ ts: liveTs, price: p_live });
    }

    // Sort chronologically
    points.sort(function(a, b) { return a.ts - b.ts; });

    // Apply selected timeframe filter
    (function() {
      var tf = lsGet("tsa_chart_timeframe", "all");
      var tfMs = { "1d": 86400000, "3d": 259200000, "7d": 604800000 }[tf];
      if (tfMs) {
        var cutoff = Date.now() - tfMs;
        points = points.filter(function(p) { return p.ts >= cutoff; });
      }
      // Highlight active button
      var btns = document.querySelectorAll(".qt-tf-btn");
      btns.forEach(function(b) {
        var active = b.getAttribute("data-tf") === tf;
        b.style.background = active ? "#2a2a4a" : "none";
        b.style.color = active ? "#e0e0ff" : "#7a7a9a";
      });
    })();

    // ── Outlier filter: drop points > 12% away from local 5-pt median ──────
    if (points.length >= 5) {
      points = points.filter(function(pt, i, arr) {
        var win = [];
        for (var j = Math.max(0, i - 2); j <= Math.min(arr.length - 1, i + 2); j++) {
          if (j !== i) win.push(arr[j].price);
        }
        if (!win.length) return true;
        win.sort(function(a, b) { return a - b; });
        var med = win[Math.floor(win.length / 2)];
        return med <= 0 || Math.abs(pt.price - med) / med < 0.12;
      });
    }

    // ── Bucket-downsample: 1 median price per 30-min slot ───────────────────
    // (collapses clustered points that cause vertical spike artifacts)
    (function() {
      var BUCKET = 30 * 60000;
      var map = {};
      points.forEach(function(pt) {
        var k = Math.floor(pt.ts / BUCKET);
        if (!map[k]) map[k] = [];
        map[k].push(pt.price);
      });
      var keys = Object.keys(map).map(Number).sort(function(a, b) { return a - b; });
      points = keys.map(function(k) {
        var arr = map[k].slice().sort(function(a, b) { return a - b; });
        return { ts: k * BUCKET + BUCKET / 2, price: arr[Math.floor(arr.length / 2)] };
      });
    })();

    if (points.length < 2) {
      container.style.display = "block";
      canvas.style.display = "none";
      var noDataEl = document.getElementById("qt-chart-nodata");
      if (!noDataEl) {
        noDataEl = document.createElement("div");
        noDataEl.id = "qt-chart-nodata";
        noDataEl.style.cssText = "padding:18px;text-align:center;font-size:11px;color:#888";
        container.appendChild(noDataEl);
      }
      noDataEl.style.display = "block";
      noDataEl.textContent = "Not enough history for " + sym + " — check back after next refresh";
      return;
    }
    canvas.style.display = "";
    var noDataEl2 = document.getElementById("qt-chart-nodata");
    if (noDataEl2) noDataEl2.style.display = "none";

    container.style.display = "block";
    if (title) title.textContent = sym + " · " + points.length + " pts · 7d";
    if (liveEl) liveEl.textContent = p_live > 0 ? "$" + p_live.toFixed(2) : "";

    // Canvas — taller to show more detail, full width
    var w = canvas.offsetWidth || 300;
    var h = 110;
    canvas.width = w;
    canvas.height = h;
    var ctx = canvas.getContext("2d");
    if (!ctx) return;
    ctx.clearRect(0, 0, w, h);

    var prices = points.map(function(p) { return p.price; });
    var minP = Math.min.apply(null, prices);
    var maxP = Math.max.apply(null, prices);
    var range = maxP - minP || maxP * 0.001 || 1;
    var padT = 14, padB = 10, padL = 2, padR = 2;
    var chartH = h - padT - padB;
    var chartW = w - padL - padR;

    // Time range for proportional x positioning
    var tMin = points[0].ts;
    var tMax = points[points.length - 1].ts;
    var tRange = tMax - tMin || 1;

    function xOf(ts) { return padL + ((ts - tMin) / tRange) * chartW; }
    function yOf(price) { return padT + (1 - (price - minP) / range) * chartH; }

    // Grid — 4 horizontal lines
    ctx.strokeStyle = "rgba(60,60,100,0.25)";
    ctx.lineWidth = 0.5;
    for (var g = 0; g <= 3; g++) {
      var gy = padT + chartH * g / 3;
      ctx.beginPath(); ctx.moveTo(padL, gy); ctx.lineTo(w - padR, gy); ctx.stroke();
    }

    // Recalculate scales after filtering/downsampling
    if (points.length < 2) return; // guard: filtering may have removed too many
    prices = points.map(function(p) { return p.price; });
    minP = Math.min.apply(null, prices);
    maxP = Math.max.apply(null, prices);
    // Add 4% padding so line never touches the very top/bottom edge
    var pricePad = (maxP - minP) * 0.04 || maxP * 0.004 || 0.01;
    minP -= pricePad; maxP += pricePad;
    range = maxP - minP || 1;
    // Re-anchor time scale to downsampled timestamps
    tMin = points[0].ts;
    tMax = points[points.length - 1].ts;
    tRange = tMax - tMin || 1;

    // Trend color
    var isUp = prices[prices.length - 1] >= prices[0];
    var lineColor = isUp ? "#4cff91" : "#ff4c6a";
    var fillColor = isUp ? "rgba(76,255,145,0.07)" : "rgba(255,76,106,0.06)";

    // Gap threshold: segments more than 4 h apart are not connected
    var GAP = 4 * 3600000;

    // Split into continuous segments respecting gaps
    var segments = [];
    var seg = [];
    for (var si = 0; si < points.length; si++) {
      if (seg.length > 0 && points[si].ts - points[si - 1].ts > GAP) {
        if (seg.length >= 2) segments.push(seg);
        seg = [];
      }
      seg.push(points[si]);
    }
    if (seg.length >= 2) segments.push(seg);

    // Helper: draw smooth open path through segment using midpoint curves
    function drawSmoothPath(pts) {
      var x0 = xOf(pts[0].ts), y0 = yOf(pts[0].price);
      ctx.moveTo(x0, y0);
      for (var i = 1; i < pts.length; i++) {
        var x1 = xOf(pts[i].ts), y1 = yOf(pts[i].price);
        var mx = (xOf(pts[i - 1].ts) + x1) / 2;
        var my = (yOf(pts[i - 1].price) + y1) / 2;
        ctx.quadraticCurveTo(xOf(pts[i - 1].ts), yOf(pts[i - 1].price), mx, my);
      }
      // End exactly at last point
      ctx.lineTo(xOf(pts[pts.length - 1].ts), yOf(pts[pts.length - 1].price));
    }

    // Fill under each segment
    segments.forEach(function(pts) {
      ctx.beginPath();
      drawSmoothPath(pts);
      ctx.lineTo(xOf(pts[pts.length - 1].ts), h - padB);
      ctx.lineTo(xOf(pts[0].ts), h - padB);
      ctx.closePath();
      ctx.fillStyle = fillColor;
      ctx.fill();
    });

    // Price line over each segment
    ctx.lineWidth = 1.5;
    ctx.lineJoin = "round";
    ctx.lineCap = "round";
    segments.forEach(function(pts) {
      ctx.beginPath();
      ctx.strokeStyle = lineColor;
      drawSmoothPath(pts);
      ctx.stroke();
    });

    // Buy markers (green ▲) from transaction history
    var ownedForChart = lastOwnedMap && lastOwnedMap[sym.toUpperCase()];
    if (ownedForChart && ownedForChart.transactions) {
      ownedForChart.transactions.forEach(function(t) {
        if (!t.bought_price || t.bought_price <= 0) return;
        var txMs = (t.time_bought || 0) * 1000;
        if (txMs < tMin || txMs > tMax) return;
        var x = xOf(txMs), y = yOf(t.bought_price);
        ctx.beginPath();
        ctx.moveTo(x, y - 5);
        ctx.lineTo(x - 4, y + 3);
        ctx.lineTo(x + 4, y + 3);
        ctx.closePath();
        ctx.fillStyle = "#4cff91";
        ctx.globalAlpha = 0.9;
        ctx.fill();
        ctx.globalAlpha = 1;
      });
    }

    // Sell markers (red ▼) from realized events
    var chartSellEvents = getRealizedEvents().filter(function(e) {
      return e.sym === sym.toUpperCase() && e.sell_price > 0;
    });
    chartSellEvents.forEach(function(e) {
      var evMs = e.ts * 1000;
      if (evMs < tMin || evMs > tMax) return;
      var x = xOf(evMs), y = yOf(e.sell_price);
      ctx.beginPath();
      ctx.moveTo(x, y + 5);
      ctx.lineTo(x - 4, y - 3);
      ctx.lineTo(x + 4, y - 3);
      ctx.closePath();
      ctx.fillStyle = "#ff4c6a";
      ctx.globalAlpha = 0.9;
      ctx.fill();
      ctx.globalAlpha = 1;
    });

    // Min/Max price labels top-right (use actual data extremes, not padded)
    var dataMax = Math.max.apply(null, prices);
    var dataMin = Math.min.apply(null, prices);
    ctx.fillStyle = "#7a7a9a";
    ctx.font = "8px JetBrains Mono, monospace";
    ctx.textAlign = "right";
    ctx.fillText("$" + dataMax.toFixed(2), w - padR - 1, padT - 3);
    ctx.fillText("$" + dataMin.toFixed(2), w - padR - 1, h - padB + 8);

    // Time labels below — first, 25%, 50%, 75%, last
    if (labelsEl) {
      function fmtTs(ts) {
        var d = new Date(ts);
        var now2 = Date.now();
        var age = now2 - ts;
        if (age < 3600000) return Math.round(age / 60000) + "m";
        if (age < 86400000) return Math.round(age / 3600000) + "h";
        return d.getDate() + "/" + (d.getMonth() + 1);
      }
      var picks = [0, Math.floor(points.length * 0.25), Math.floor(points.length * 0.5), Math.floor(points.length * 0.75), points.length - 1];
      // Deduplicate picks
      picks = picks.filter(function(v, i, a) { return a.indexOf(v) === i; });
      labelsEl.style.position = "relative";
      labelsEl.innerHTML = picks.map(function(idx) {
        var pt = points[idx];
        var xPct = ((pt.ts - tMin) / tRange * 100).toFixed(1);
        var isLast = idx === points.length - 1;
        return '<span style="position:absolute;left:' + xPct + '%;transform:translateX(-50%);' + (isLast ? 'color:#4cff91' : '') + '">' +
          (isLast ? 'Now' : fmtTs(pt.ts)) + '</span>';
      }).join('');
    }

    // Hover tooltip
    (function() {
      var tip = document.getElementById("qt-chart-tip");
      if (!tip) {
        tip = document.createElement("div");
        tip.id = "qt-chart-tip";
        tip.style.cssText = "position:absolute;pointer-events:none;display:none;background:rgba(10,10,20,0.92);border:1px solid #3a3a6a;border-radius:6px;padding:5px 8px;font-family:JetBrains Mono,monospace;font-size:10px;color:#e0e0ff;z-index:9999;white-space:nowrap;";
        container.style.position = "relative";
        container.appendChild(tip);
      }
      canvas.onmousemove = function(ev) {
        var rect = canvas.getBoundingClientRect();
        var mx = ev.clientX - rect.left;
        var scaleX = canvas.width / rect.width;
        var px = mx * scaleX;
        // Find nearest point
        var best = null, bestDist = Infinity;
        points.forEach(function(pt) {
          var x = padL + ((pt.ts - tMin) / tRange) * chartW;
          var dist = Math.abs(x - px);
          if (dist < bestDist) { bestDist = dist; best = pt; }
        });
        if (!best) { tip.style.display = "none"; return; }
        var tipX = mx + 10;
        if (tipX + 120 > rect.width) tipX = mx - 130;
        tip.style.left = tipX + "px";
        tip.style.top = "4px";
        tip.style.display = "block";
        var d2 = new Date(best.ts);
        var timeStr = d2.getDate() + "/" + (d2.getMonth()+1) + " " +
          ("0"+d2.getHours()).slice(-2) + ":" + ("0"+d2.getMinutes()).slice(-2);
        tip.textContent = "$" + best.price.toFixed(2) + "  " + timeStr;
      };
      canvas.onmouseleave = function() { tip.style.display = "none"; };
    })();
  }

  function applyQtTheme(isDark) {
    var bar = document.getElementById("qt-bar");
    if (!bar) return;
    var bg       = isDark ? "#0c0c14"        : "#f0f4ff";
    var border   = isDark ? "#3a3a6a"        : "#c0d0ff";
    var selBg    = isDark ? "#13131f"        : "#ffffff";
    var selColor = isDark ? "#e0e0ff"        : "#222222";
    var selBord  = isDark ? "#2a2a4a"        : "#c0d0ee";
    var chartBg  = isDark ? "#0a0a12"        : "#f7f9fc";
    var chartBrd = isDark ? "#2a2a4a"        : "#dde3f0";
    var labelCol = isDark ? "#7a7a9a"        : "#666666";
    var liveCol  = isDark ? "#e0e0ff"        : "#222222";

    bar.style.cssText = bar.style.cssText
      .replace(/background:[^;]+/, "background:" + bg)
      .replace(/border-bottom:[^;]+/, "border-bottom:2px solid " + border);

    var chart = document.getElementById("qt-chart-container");
    if (chart) {
      chart.style.background = chartBg;
      chart.style.border = "1px solid " + chartBrd;
    }
    var sel = document.getElementById("qt-stock-search");
    if (sel) {
      sel.style.background = selBg;
      sel.style.color = selColor;
      sel.style.border = "1px solid " + selBord;
    }
    var listEl = document.getElementById("qt-stock-list");
    if (listEl) {
      listEl.style.background = selBg;
      listEl.style.border = "1px solid " + selBord;
    }
    var titleEl = document.getElementById("qt-chart-title");
    if (titleEl) titleEl.style.color = labelCol;
    var liveEl = document.getElementById("qt-chart-live");
    if (liveEl) liveEl.style.color = liveCol;
    var labelsEl = document.getElementById("qt-chart-labels");
    if (labelsEl) labelsEl.style.color = labelCol;
    // Benefit Lock label — yellow is invisible on light backgrounds
    var lockLabel = document.getElementById("qt-lock-label");
    var lockText  = document.getElementById("qt-lock-text");
    if (lockLabel) {
      lockLabel.style.border      = isDark ? "1px solid rgba(255,193,7,0.35)" : "1px solid #c8930a";
      lockLabel.style.background  = isDark ? "rgba(255,193,7,0.08)"           : "rgba(180,120,0,0.08)";
    }
    if (lockText) {
      lockText.style.color = isDark ? "#ffc107" : "#8a5c00";
    }
    var minBtn = document.getElementById("qt-min-btn");
    if (minBtn) {
      minBtn.style.color = isDark ? "#6a6a9a" : "#555577";
      minBtn.style.borderColor = isDark ? "#2a2a4a" : "#c0d0ee";
    }
    // Re-render buttons with new theme colors
    renderQtRows();
  }

  function createQuickTradeBar() {
    var bar = document.createElement("div");
    bar.id = "qt-bar";
    bar.style.cssText = "background:#0c0c14;border-bottom:2px solid #3a3a6a;padding:8px 12px;box-shadow:0 4px 16px rgba(0,0,0,0.5);font-family:JetBrains Mono,monospace;position:sticky;top:0;z-index:9999;";
    bar.innerHTML =
      // Row 1: Minimize button + Stock searchable combobox + edit + lock
      "<div style='display:flex;gap:7px;margin-bottom:6px;align-items:center'>" +
        "<button id='qt-min-btn' title='Minimer Quick Trade bar' style='padding:4px 7px;border-radius:7px;border:1px solid #2a2a4a;background:transparent;color:#6a6a9a;font-family:JetBrains Mono,monospace;font-size:10px;cursor:pointer;flex-shrink:0;'>&#9660;</button>" +
        "<div id='qt-stock-wrap' style='position:relative;flex:1'>" +
          "<input id='qt-stock-search' type='text' placeholder='Search stock…' autocomplete='off' style='width:100%;box-sizing:border-box;background:#13131f;border:1px solid #2a2a4a;border-radius:7px;color:#e0e0ff;font-family:JetBrains Mono,monospace;font-size:12px;font-weight:700;padding:7px 26px 7px 10px;outline:none;'>" +
          "<input type='hidden' id='qt-stock' value=''>" +
          "<span style='position:absolute;right:9px;top:50%;transform:translateY(-50%);color:#6a6a9a;font-size:9px;pointer-events:none'>▼</span>" +
          "<div id='qt-stock-list' style='display:none;position:absolute;top:calc(100% + 3px);left:0;right:0;background:#13131f;border:1px solid #2a2a4a;border-radius:7px;z-index:99999;max-height:200px;overflow-y:auto;box-shadow:0 4px 16px rgba(0,0,0,0.5);'></div>" +
        "</div>" +
        "<button id='qt-edit' title='Edit trade amounts' style='padding:6px 8px;border-radius:7px;border:1px solid #2a2a4a;background:transparent;color:#6a6a9a;font-family:JetBrains Mono,monospace;font-size:10px;cursor:pointer;flex-shrink:0;'>✎</button>" +
        "<label id='qt-lock-label' style='display:flex;align-items:center;gap:5px;cursor:pointer;flex-shrink:0;padding:4px 8px;border-radius:7px;border:1px solid rgba(255,193,7,0.35);background:rgba(255,193,7,0.08);'>" +
          "<input type='checkbox' id='qt-lock-benefit' checked style='accent-color:#ffc107;width:13px;height:13px;cursor:pointer'>" +
          "<span id='qt-lock-text' style='font-size:10px;font-weight:700;color:#ffc107;font-family:JetBrains Mono,monospace;letter-spacing:0.04em;white-space:nowrap;'>🔒 Benefit Lock</span>" +
        "</label>" +
      "</div>" +
      // Collapsible body: benefit info + buy/sell rows + rec + chart
      "<div id='qt-body'>" +
        "<div id='qt-benefit-info' style='display:none;margin-bottom:4px;padding:0 2px;'>" +
          "<span id='qt-swing-available' style='font-size:10px;color:#ffc107;font-family:JetBrains Mono,monospace;font-weight:600;'></span>" +
        "</div>" +
        // Row 2: BUY buttons
        "<div style='display:flex;gap:5px;align-items:center;margin-bottom:5px'>" +
          "<span style='font-size:9px;font-weight:700;color:#4cff91;font-family:JetBrains Mono,monospace;flex-shrink:0;letter-spacing:0.06em;'>▲</span>" +
          "<div id='qt-buy-row' style='display:flex;gap:5px;flex:1;overflow-x:auto;padding-bottom:2px'></div>" +
        "</div>" +
        // Row 3: SELL buttons
        "<div style='display:flex;gap:5px;align-items:center'>" +
          "<span style='font-size:9px;font-weight:700;color:#ff4c6a;font-family:JetBrains Mono,monospace;flex-shrink:0;letter-spacing:0.06em;'>▼</span>" +
          "<div id='qt-sell-row' style='display:flex;gap:5px;flex:1;overflow-x:auto;padding-bottom:2px'></div>" +
        "</div>" +
        // ROI recommendation
        "<div id='qt-rec' style='display:none;margin-top:6px;'></div>" +
        // Chart
        "<div id='qt-chart-container' style='display:none;margin-top:8px;background:#0a0a12;border-radius:8px;border:1px solid #2a2a4a;padding:8px 10px 6px;'>" +
          "<div style='display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;'>" +
            "<span id='qt-chart-title' style='font-size:9px;font-weight:700;color:#7a7a9a;font-family:JetBrains Mono,monospace;letter-spacing:0.08em;text-transform:uppercase;'></span>" +
            "<span id='qt-chart-live' style='font-size:10px;font-weight:700;font-family:JetBrains Mono,monospace;color:#e0e0ff;'></span>" +
          "</div>" +
          "<canvas id='qt-chart-canvas' height='110' style='width:100%;height:110px;display:block;'></canvas>" +
          "<div id='qt-chart-labels' style='position:relative;height:14px;font-size:8px;color:#6a6a9a;font-family:JetBrains Mono,monospace;margin-top:2px;'></div>" +
          "<div id='qt-tf-row' style='display:flex;gap:3px;margin-top:5px;'>" +
            "<button class='qt-tf-btn' data-tf='1d' style='flex:1;padding:2px 0;border-radius:4px;border:1px solid #2a2a4a;background:none;color:#7a7a9a;font-size:9px;font-family:JetBrains Mono,monospace;cursor:pointer;'>1d</button>" +
            "<button class='qt-tf-btn' data-tf='3d' style='flex:1;padding:2px 0;border-radius:4px;border:1px solid #2a2a4a;background:none;color:#7a7a9a;font-size:9px;font-family:JetBrains Mono,monospace;cursor:pointer;'>3d</button>" +
            "<button class='qt-tf-btn' data-tf='7d' style='flex:1;padding:2px 0;border-radius:4px;border:1px solid #2a2a4a;background:none;color:#7a7a9a;font-size:9px;font-family:JetBrains Mono,monospace;cursor:pointer;'>7d</button>" +
            "<button class='qt-tf-btn' data-tf='all' style='flex:1;padding:2px 0;border-radius:4px;border:1px solid #2a2a4a;background:none;color:#7a7a9a;font-size:9px;font-family:JetBrains Mono,monospace;cursor:pointer;'>All</button>" +
          "</div>" +
        "</div>" +
      "</div>";

    var target = document.getElementById("stockmarketroot") ||
                 document.querySelector(".content-wrapper") ||
                 document.querySelector("#page-wrapper > div") ||
                 document.body;
    if (target && target.firstChild) target.insertBefore(bar, target.firstChild);
    else document.body.insertBefore(bar, document.body.firstChild);

    renderQtRows();
    setTimeout(qtBuildMaps, 1000);
    setTimeout(function() { updateQtRecommendation(null); }, 1500);
    // Apply initial theme
    applyQtTheme(lsGet("tsa_dark", "false") === "true");
    // Apply initial minimized state
    (function() {
      var minimized = lsGet("qt_minimized", "false") === "true";
      var qtBody = document.getElementById("qt-body");
      var qtMinBtn = document.getElementById("qt-min-btn");
      if (qtBody) qtBody.style.display = minimized ? "none" : "block";
      if (qtMinBtn) qtMinBtn.textContent = minimized ? "\u25B6" : "\u25BC";
    })();

    // Timeframe button click handlers
    document.querySelectorAll(".qt-tf-btn").forEach(function(btn) {
      btn.addEventListener("click", function() {
        lsSet("tsa_chart_timeframe", this.getAttribute("data-tf"));
        var sym = lsGet("qt_last_stock", "");
        if (sym) qtDrawChart(sym);
      });
    });

    // Searchable combobox logic
    (function() {
      var searchEl  = document.getElementById("qt-stock-search");
      var hiddenEl  = document.getElementById("qt-stock");
      var listEl    = document.getElementById("qt-stock-list");
      if (!searchEl || !hiddenEl || !listEl) return;

      function buildList(filter) {
        var f = (filter || "").toUpperCase().trim();
        var items = f ? QT_STOCKS.filter(function(s){ return s.indexOf(f) >= 0; }) : QT_STOCKS;
        var isDarkList = lsGet("tsa_dark", "false") === "true";
        listEl.innerHTML = "";
        items.forEach(function(sym) {
          var item = document.createElement("div");
          item.className = "qt-stock-list-item";
          item.textContent = sym;
          item.style.color = isDarkList ? "#e0e0ff" : "#222222";
          item.onmousedown = function(e) {
            e.preventDefault(); // prevent blur before click fires
            hiddenEl.value = sym;
            searchEl.value = sym;
            listEl.style.display = "none";
            lsSet("qt_last_stock", sym);
            qtDrawChart(sym);
            qtUpdateExec();
          };
          listEl.appendChild(item);
        });
        listEl.style.display = items.length ? "block" : "none";
      }

      searchEl.addEventListener("focus", function() { this.select(); buildList(""); });
      searchEl.addEventListener("input", function() {
        hiddenEl.value = "";
        buildList(searchEl.value);
        qtUpdateExec();
      });
      searchEl.addEventListener("blur", function() {
        // Small delay so mousedown on item fires first
        setTimeout(function() { listEl.style.display = "none"; }, 150);
      });
      searchEl.addEventListener("keydown", function(e) {
        if (e.key === "Escape") { listEl.style.display = "none"; searchEl.blur(); }
      });

      // Restore last selected stock
      var lastStock = lsGet("qt_last_stock", "");
      if (lastStock) {
        hiddenEl.value = lastStock;
        searchEl.value = lastStock;
        qtDrawChart(lastStock);
      }
    })();

    $("#qt-edit").on("click", function() {
      qtEditMode = !qtEditMode;
      $(this).css({color: qtEditMode ? "#4a6fa5" : "#6a6a9a", borderColor: qtEditMode ? "#4a6fa5" : "#2a2a4a"});
      $(this).text(qtEditMode ? "✓" : "✎");
      renderQtRows();
    });

    document.getElementById("qt-lock-benefit") && document.getElementById("qt-lock-benefit").addEventListener("change", function() {
      qtUpdateExec();
    });

    document.getElementById("qt-min-btn") && document.getElementById("qt-min-btn").addEventListener("click", function() {
      var minimized = lsGet("qt_minimized", "false") === "true";
      minimized = !minimized;
      lsSet("qt_minimized", String(minimized));
      var body = document.getElementById("qt-body");
      if (body) body.style.display = minimized ? "none" : "block";
      this.textContent = minimized ? "\u25B6" : "\u25BC";
    });
  }

  function createUI() {
    injectStyles();
    // Extra CSS for v1.8.0 UI features
    var extraStyle = document.createElement("style");
    extraStyle.textContent = [
      "@keyframes tsaToastIn{from{opacity:0;transform:translateX(-20px)}to{opacity:1;transform:translateX(0)}}",
      "@keyframes tsaSlideIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}",
      "#tsa-overlay.tsa-visible{animation:tsaSlideIn 0.18s ease forwards}",
      ".tsa-pin-btn{background:none;border:none;cursor:pointer;font-size:12px;padding:0 2px;opacity:0.35;transition:opacity 0.15s;line-height:1;vertical-align:middle}",
      ".tsa-pin-btn.pinned{opacity:1}",
      ".tsa-pin-btn:hover{opacity:1}",
      "#tsa-scroll-top{position:sticky;bottom:8px;float:right;margin-right:8px;z-index:10;background:#4a6fa5;color:#fff;border:none;border-radius:50%;width:26px;height:26px;font-size:13px;cursor:pointer;display:none;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,0.35);line-height:1}",
      "#tsa-scroll-top:hover{background:#3a5f95}",
      "#tsa-overlay.tsa-dark #tsa-scroll-top{background:#2a2a5a;color:#a0a0ff}",
      ".qt-stock-list-item{padding:7px 10px;cursor:pointer;font-size:12px;font-weight:700;font-family:JetBrains Mono,monospace}",
      ".qt-stock-list-item:hover{background:rgba(122,159,212,0.15)}"
    ].join("\n");
    document.head.appendChild(extraStyle);

    // Create Quick Trade bar embedded in page
    createQuickTradeBar();
    var btn = document.createElement("button");
    btn.id = "tsa-btn";
    btn.textContent = "Stocks";
    document.body.appendChild(btn);

    var overlay = document.createElement("div");
    overlay.id = "tsa-overlay";
    // Dynamic width: mobile uses available screen width, desktop capped at 420px
    var isMobile = /Mobi|Android/i.test(navigator.userAgent);
    overlay.style.width = isMobile ? (window.innerWidth - 32) + "px" : "420px";
    if (lsGet("tsa_dark", "false") === "true") overlay.classList.add("tsa-dark");
    var isDarkInit = lsGet("tsa_dark", "false") === "true";
    overlay.innerHTML =
      "<div class=\"tsa-header\">" +
        "<div class=\"tsa-header-left\">" +
          "<span class=\"tsa-title\">TORN STOCK ANALYZER</span>" +
          "<button class=\"tsa-theme-btn\" id=\"tsa-theme-btn\" title=\"Toggle theme\">" + (isDarkInit ? "[L]" : "[D]") + "</button>" +
          "<button class=\"tsa-theme-btn\" id=\"tsa-roi-btn\" title=\"ROI Planner\">📊</button>" +
          "<button class=\"tsa-theme-btn\" id=\"tsa-alerts-btn\" title=\"Price Alerts\">🔔</button>" +
          "<button class=\"tsa-theme-btn\" id=\"tsa-apikey-btn\" title=\"Set API Key\">🔑</button>" +
          "<button class=\"tsa-theme-btn\" id=\"tsa-settings-btn\" title=\"Settings\">⚙️</button>" +
          "<button class=\"tsa-theme-btn\" id=\"tsa-update-btn\" title=\"Update all\">↻</button>" +
        "</div>" +
        "<span class=\"tsa-close\" id=\"tsa-close\">x</span>" +
      "</div>" +
      "<div id=\"tsa-content\">" +
        "<div class=\"tsa-loading\">Ready to analyze</div>" +
        "<div class=\"tsa-footer\"><span></span><button class=\"tsa-refresh\" id=\"tsa-init-btn\">Start</button></div>" +
      "</div>";
    document.body.appendChild(overlay);
    applyOverlayPosition(getOverlayPosition());

    document.getElementById("tsa-theme-btn").addEventListener("click", function() {
      var isDark = overlay.classList.toggle("tsa-dark");
      lsSet("tsa_dark", isDark.toString());
      document.getElementById("tsa-theme-btn").textContent = isDark ? "[L]" : "[D]";
      // Sync quick trade bar theme
      applyQtTheme(isDark);
      // Re-render content with new colours
      if (roiPlannerActive && lastOwnedMap) {
        showROIPlanner(lastOwnedMap, lastRaw);
      } else if (lastOwnedMap) {
        loadData();
      }
    });

    document.getElementById("tsa-apikey-btn").addEventListener("click", function() {
      var content = document.getElementById("tsa-content");
      showKeyOnboarding(content, function() {
        showToast("API key saved.", "success");
        loadData();
      });
    });

    document.getElementById("tsa-settings-btn").addEventListener("click", function() {
      var content = document.getElementById("tsa-content");
      var isDarkNow = overlay.classList.contains("tsa-dark");
      var bg  = isDarkNow ? "#0f0f1a" : "#ffffff";
      var bg2 = isDarkNow ? "#1a1a2e" : "#f7f9fc";
      var border = isDarkNow ? "#2a2a4a" : "#eee";
      var text = isDarkNow ? "#c8c8d8" : "#222";
      var muted = isDarkNow ? "#7a7a9a" : "#666";

      content.innerHTML =
        "<div style=\"padding:14px\">" +
        "<div style=\"font-size:10px;letter-spacing:0.12em;color:" + muted + ";text-transform:uppercase;font-weight:bold;margin-bottom:14px\">Settings</div>" +

        "<div style=\"margin-bottom:12px\">" +
        "<div style=\"font-size:11px;color:" + muted + ";margin-bottom:4px\">Profit target (%)</div>" +
        "<input id=\"tsa-setting-profit\" type=\"number\" step=\"0.1\" min=\"0.1\" max=\"10\" value=\"" + getProfitTarget() + "\" style=\"width:100%;padding:7px 10px;border-radius:7px;border:1px solid " + border + ";background:" + bg2 + ";color:" + text + ";font-size:13px;\">" +
        "</div>" +

        "<div style=\"margin-bottom:12px\">" +
        "<div style=\"font-size:11px;color:" + muted + ";margin-bottom:4px\">Stop loss (%)</div>" +
        "<input id=\"tsa-setting-stoploss\" type=\"number\" step=\"0.1\" min=\"0.1\" max=\"20\" value=\"" + getStopLoss() + "\" style=\"width:100%;padding:7px 10px;border-radius:7px;border:1px solid " + border + ";background:" + bg2 + ";color:" + text + ";font-size:13px;\">" +
        "</div>" +

        "<div style=\"margin-bottom:12px\">" +
        "<div style=\"font-size:11px;color:" + muted + ";margin-bottom:4px\">Auto-refresh (min, 0 = off)</div>" +
        "<input id=\"tsa-setting-autorefresh\" type=\"number\" step=\"1\" min=\"0\" max=\"60\" value=\"" + getAutoRefreshInterval() + "\" style=\"width:100%;padding:7px 10px;border-radius:7px;border:1px solid " + border + ";background:" + bg2 + ";color:" + text + ";font-size:13px;\">" +
        "</div>" +

        "<div style=\"margin-bottom:16px\">" +
        "<div style=\"font-size:11px;color:" + muted + ";margin-bottom:4px\">Price history (days, 1–30)</div>" +
        "<input id=\"tsa-setting-histdays\" type=\"number\" step=\"1\" min=\"1\" max=\"30\" value=\"" + parseInt(lsGet("tsa_history_days", "30"), 10) + "\" style=\"width:100%;padding:7px 10px;border-radius:7px;border:1px solid " + border + ";background:" + bg2 + ";color:" + text + ";font-size:13px;\">" +
        "</div>" +

        "<div style=\"border-top:1px solid " + border + ";margin-bottom:12px;padding-top:12px\">" +
        "<div style=\"font-size:10px;letter-spacing:0.1em;color:" + muted + ";text-transform:uppercase;font-weight:bold;margin-bottom:10px\">Buy signals</div>" +
        "<label style=\"display:flex;align-items:center;gap:8px;cursor:pointer;margin-bottom:10px\">" +
          "<input type=\"checkbox\" id=\"tsa-setting-show-watch\"" + (getShowWatch() ? " checked" : "") + " style=\"width:15px;height:15px;cursor:pointer\">" +
          "<span style=\"font-size:12px;color:" + text + "\">Show Watch section</span>" +
        "</label>" +
        "<div style=\"margin-bottom:12px\">" +
          "<div style=\"font-size:11px;color:" + muted + ";margin-bottom:4px\">Min score for Top 5 (0–160)</div>" +
          "<input id=\"tsa-setting-top5-min\" type=\"number\" step=\"1\" min=\"0\" max=\"160\" value=\"" + getTop5MinScore() + "\" style=\"width:100%;padding:7px 10px;border-radius:7px;border:1px solid " + border + ";background:" + bg2 + ";color:" + text + ";font-size:13px;\">" +
        "</div>" +
        "<label style=\"display:flex;align-items:center;gap:8px;cursor:pointer;margin-bottom:10px\">" +
          "<input type=\"checkbox\" id=\"tsa-setting-req-investors\"" + (getRequirePositiveInvestors() ? " checked" : "") + " style=\"width:15px;height:15px;cursor:pointer;accent-color:#4a6fa5\">" +
          "<span style=\"font-size:12px;color:" + text + "\">Kræv positiv investor-delta i Top 5 Buy</span>" +
        "</label>" +
        "</div>" +

        "<div style=\"border-top:1px solid " + border + ";margin-bottom:12px;padding-top:12px\">" +
        "<div style=\"font-size:10px;letter-spacing:0.1em;color:" + muted + ";text-transform:uppercase;font-weight:bold;margin-bottom:10px\">Trading profit</div>" +
        "<label style=\"display:flex;align-items:center;gap:8px;cursor:pointer;margin-bottom:10px\">" +
          "<input type=\"checkbox\" id=\"tsa-setting-swing-only\"" + (getProfitSwingOnly() ? " checked" : "") + " style=\"width:15px;height:15px;cursor:pointer\">" +
          "<span style=\"font-size:12px;color:" + text + "\">Swing trade profit only</span>" +
        "</label>" +
        "<label style=\"display:flex;align-items:center;gap:8px;cursor:pointer;margin-bottom:10px\">" +
          "<input type=\"checkbox\" id=\"tsa-setting-show-realized\"" + (getShowRealized() ? " checked" : "") + " style=\"width:15px;height:15px;cursor:pointer\">" +
          "<span style=\"font-size:12px;color:" + text + "\">Show realized profit</span>" +
        "</label>" +
        "<div id=\"tsa-realized-options\" style=\"display:" + (getShowRealized() ? "block" : "none") + ";padding-left:23px\">" +
          "<div style=\"font-size:11px;color:" + muted + ";margin-bottom:4px\">Period (days, 1–90)</div>" +
          "<input id=\"tsa-setting-realized-days\" type=\"number\" step=\"1\" min=\"1\" max=\"90\" value=\"" + getRealizedDays() + "\" style=\"width:100%;padding:7px 10px;border-radius:7px;border:1px solid " + border + ";background:" + bg2 + ";color:" + text + ";font-size:13px;margin-bottom:8px\">" +
          "<button id=\"tsa-realized-reset\" style=\"width:100%;padding:7px;border-radius:7px;border:1px solid " + border + ";background:none;color:" + muted + ";font-size:12px;cursor:pointer\">Reset realized profit</button>" +
        "</div>" +
        "</div>" +

        "<div style=\"margin-bottom:12px\">" +
          "<div style=\"font-size:11px;color:" + muted + ";margin-bottom:4px\">Overlay position</div>" +
          "<select id=\"tsa-setting-position\" style=\"width:100%;padding:7px 10px;border-radius:7px;border:1px solid " + border + ";background:" + bg2 + ";color:" + text + ";font-size:13px;\">" +
            "<option value=\"bottom-right\"" + (getOverlayPosition() === "bottom-right" ? " selected" : "") + ">Bottom right</option>" +
            "<option value=\"bottom-left\""  + (getOverlayPosition() === "bottom-left"  ? " selected" : "") + ">Bottom left</option>" +
            "<option value=\"top-right\""    + (getOverlayPosition() === "top-right"    ? " selected" : "") + ">Top right</option>" +
            "<option value=\"top-left\""     + (getOverlayPosition() === "top-left"     ? " selected" : "") + ">Top left</option>" +
          "</select>" +
        "</div>" +

        "<div style=\"display:flex;gap:8px\">" +
        "<button id=\"tsa-settings-save\" style=\"flex:1;padding:8px;border-radius:7px;border:none;background:#4a6fa5;color:#fff;font-size:13px;font-weight:bold;cursor:pointer\">Save</button>" +
        "<button id=\"tsa-settings-cancel\" style=\"flex:1;padding:8px;border-radius:7px;border:1px solid " + border + ";background:none;color:" + muted + ";font-size:13px;cursor:pointer\">Cancel</button>" +
        "</div>" +
        "</div>";

      // Toggle realized options visibility
      document.getElementById("tsa-setting-show-realized").addEventListener("change", function() {
        document.getElementById("tsa-realized-options").style.display = this.checked ? "block" : "none";
      });

      document.getElementById("tsa-realized-reset").addEventListener("click", function() {
        lsSet("tsa_realized_events", "[]");
        lsSet("tsa_prev_holdings", "{}");
        showToast("Realized profit reset");
        loadData();
      });

      document.getElementById("tsa-settings-save").addEventListener("click", function() {
        var profit = parseFloat(document.getElementById("tsa-setting-profit").value);
        var stop = parseFloat(document.getElementById("tsa-setting-stoploss").value);
        var ar = parseInt(document.getElementById("tsa-setting-autorefresh").value, 10);
        var hd = parseInt(document.getElementById("tsa-setting-histdays").value, 10);
        var swingOnly = document.getElementById("tsa-setting-swing-only").checked;
        var showRealized = document.getElementById("tsa-setting-show-realized").checked;
        var showWatch = document.getElementById("tsa-setting-show-watch").checked;
        var top5Min = parseInt(document.getElementById("tsa-setting-top5-min").value, 10);
        var reqInv = document.getElementById("tsa-setting-req-investors").checked;
        var rd = parseInt((document.getElementById("tsa-setting-realized-days") || {}).value || "7", 10);
        var posVal = document.getElementById("tsa-setting-position").value;
        if (isNaN(profit) || profit <= 0) { showToast("Invalid profit target", "warn"); return; }
        if (isNaN(stop) || stop <= 0) { showToast("Invalid stop loss", "warn"); return; }
        if (isNaN(ar) || ar < 0) { showToast("Invalid auto-refresh interval", "warn"); return; }
        if (isNaN(hd) || hd < 1 || hd > 30) { showToast("History must be 1–30 days", "warn"); return; }
        if (isNaN(top5Min) || top5Min < 0 || top5Min > 160) { showToast("Min score must be 0–160", "warn"); return; }
        if (showRealized && (isNaN(rd) || rd < 1 || rd > 90)) { showToast("Period must be 1–90 days", "warn"); return; }
        lsSet("tsa_profit_target", profit.toString());
        lsSet("tsa_stop_loss", stop.toString());
        lsSet("tsa-auto-refresh-interval", ar.toString());
        lsSet("tsa_history_days", hd.toString());
        lsSet("tsa_profit_swing_only", swingOnly ? "true" : "false");
        lsSet("tsa_show_watch", showWatch ? "true" : "false");
        lsSet("tsa_top5_min_score", top5Min.toString());
        lsSet("tsa_show_realized", showRealized ? "true" : "false");
        lsSet("tsa_require_positive_investors", reqInv ? "true" : "false");
        if (showRealized && !isNaN(rd)) lsSet("tsa_realized_days", rd.toString());
        lsSet("tsa_overlay_position", posVal);
        applyOverlayPosition(posVal);
        var saveBtn = document.getElementById("tsa-settings-save");
        if (saveBtn) {
          saveBtn.textContent = "Saved ✓";
          saveBtn.style.background = "#1a8a45";
          setTimeout(function() { loadData(); }, 700);
        } else {
          loadData();
        }
      });

      document.getElementById("tsa-settings-cancel").addEventListener("click", function() {
        loadData();
      });
    });

    document.getElementById("tsa-update-btn").addEventListener("click", function() {
      loadData();
    });

    document.getElementById("tsa-alerts-btn").addEventListener("click", function() {
      requestNotificationPermission();
      var content = document.getElementById("tsa-content");
      var isDarkNow = overlay.classList.contains("tsa-dark");
      var bg2 = isDarkNow ? "#1a1a2e" : "#f7f9fc";
      var border = isDarkNow ? "#2a2a4a" : "#eee";
      var text = isDarkNow ? "#c8c8d8" : "#222";
      var muted = isDarkNow ? "#7a7a9a" : "#666";
      var alerts = loadAlerts();

      var rows = alerts.length ? alerts.map(function(a) {
        var repeatBadge = a.repeat
          ? "<span title=\"Repeating alert\" style=\"font-size:10px;margin-left:4px;opacity:0.7\">🔁</span>"
          : "";
        return "<div style=\"display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid " + border + ";font-size:12px;\">" +
          "<span style=\"color:" + text + ";font-weight:bold\">" + a.sym + repeatBadge + "</span>" +
          "<span style=\"color:" + muted + "\">" + (a.dir === "above" ? "≥" : "≤") + " $" + parseFloat(a.price).toFixed(2) + "</span>" +
          "<button data-sym=\"" + a.sym + "\" data-dir=\"" + a.dir + "\" class=\"tsa-alert-del\" style=\"border:none;background:none;color:#cc2222;cursor:pointer;font-size:14px;\">✕</button>" +
          "</div>";
      }).join("") : "<div style=\"color:" + muted + ";font-size:11px;padding:8px 0\">No active alerts</div>";

      var stockOpts = ["ASS","BAG","CBD","CNC","ELT","EVL","EWM","FHG","GRN","HRG","IIL","IOU","IST","LAG","LOS","LSC","MCS","MSG","MUN","PRN","PTS","SYM","SYS","TCC","TCI","TCM","TCP","TCT","TGP","THS","TMI","TSB","WLT","WSU","YAZ"]
        .map(function(s) { return "<option value=\"" + s + "\">" + s + "</option>"; }).join("");

      content.innerHTML =
        "<div style=\"padding:14px\">" +
        "<div style=\"font-size:10px;letter-spacing:0.12em;color:" + muted + ";text-transform:uppercase;font-weight:bold;margin-bottom:14px\">Price Alerts</div>" +
        "<div style=\"margin-bottom:14px\">" + rows + "</div>" +
        "<div style=\"display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:6px;margin-bottom:6px;align-items:center\">" +
        "<select id=\"tsa-alert-sym\" style=\"padding:6px;border-radius:7px;border:1px solid " + border + ";background:" + bg2 + ";color:" + text + ";font-size:12px\">" + stockOpts + "</select>" +
        "<input id=\"tsa-alert-price\" type=\"number\" step=\"0.01\" min=\"0\" placeholder=\"Price\" style=\"padding:6px;border-radius:7px;border:1px solid " + border + ";background:" + bg2 + ";color:" + text + ";font-size:12px\">" +
        "<select id=\"tsa-alert-dir\" style=\"padding:6px;border-radius:7px;border:1px solid " + border + ";background:" + bg2 + ";color:" + text + ";font-size:12px\"><option value=\"above\">≥ Over</option><option value=\"below\">≤ Under</option></select>" +
        "<button id=\"tsa-alert-add\" style=\"padding:6px 10px;border-radius:7px;border:none;background:#4a6fa5;color:#fff;font-size:13px;font-weight:bold;cursor:pointer\">+</button>" +
        "</div>" +
        "<label style=\"display:flex;align-items:center;gap:6px;font-size:11px;color:" + muted + ";margin-bottom:10px;cursor:pointer\">" +
        "<input type=\"checkbox\" id=\"tsa-alert-repeat\" style=\"cursor:pointer\"> 🔁 Repeat — keep alert active after it fires" +
        "</label>" +
        "<button id=\"tsa-alerts-back\" style=\"width:100%;padding:8px;border-radius:7px;border:1px solid " + border + ";background:none;color:" + muted + ";font-size:13px;cursor:pointer\">Back</button>" +
        "</div>";

      document.getElementById("tsa-alert-add").addEventListener("click", function() {
        var sym = document.getElementById("tsa-alert-sym").value;
        var price = parseFloat(document.getElementById("tsa-alert-price").value);
        var dir = document.getElementById("tsa-alert-dir").value;
        var repeat = document.getElementById("tsa-alert-repeat").checked;
        if (!sym || isNaN(price) || price <= 0) { showToast("Enter a stock and valid price", "warn"); return; }
        addAlert(sym, price, dir, repeat);
        document.getElementById("tsa-alerts-btn").click();
      });

      document.querySelectorAll(".tsa-alert-del").forEach(function(btn) {
        btn.addEventListener("click", function() {
          removeAlert(btn.dataset.sym, btn.dataset.dir);
          document.getElementById("tsa-alerts-btn").click();
        });
      });

      document.getElementById("tsa-alerts-back").addEventListener("click", function() { loadData(); });
    });

    document.getElementById("tsa-roi-btn").addEventListener("click", function() {
      roiPlannerActive = !roiPlannerActive;
      document.getElementById("tsa-roi-btn").style.opacity = roiPlannerActive ? "1" : "0.7";
      if (roiPlannerActive && lastOwnedMap) {
        showROIPlanner(lastOwnedMap, lastRaw);
      } else if (!roiPlannerActive) {
        loadData();
      }
    });

    document.body.appendChild(overlay);

    btn.addEventListener("click", function() {
      var isOpen = overlay.style.display === "block";
      if (isOpen) {
        overlay.style.display = "none";
        overlay.classList.remove("tsa-visible");
      } else {
        overlay.style.display = "block";
        overlay.classList.remove("tsa-visible");
        // Force reflow so animation triggers fresh
        void overlay.offsetWidth;
        overlay.classList.add("tsa-visible");
        loadData();
      }
    });
    document.getElementById("tsa-close").addEventListener("click", function() {
      overlay.style.display = "none";
      overlay.classList.remove("tsa-visible");
    });

    // Swipe-down to close on mobile — only triggers when the swipe starts in
    // the top 60px of the overlay (the header area), so normal scrolling inside
    // the list is never accidentally intercepted.
    if (/Mobi|Android/i.test(navigator.userAgent)) {
      var swipeTouchStartY = 0;
      var swipeTouchStartOverlayY = 0;
      overlay.addEventListener("touchstart", function(e) {
        var overlayTop = overlay.getBoundingClientRect().top;
        swipeTouchStartY = e.touches[0].clientY;
        swipeTouchStartOverlayY = swipeTouchStartY - overlayTop;
      }, { passive: true });
      overlay.addEventListener("touchend", function(e) {
        if (swipeTouchStartOverlayY > 60) return; // started below header — ignore
        var deltaY = e.changedTouches[0].clientY - swipeTouchStartY;
        if (deltaY >= 80) {
          overlay.style.display = "none";
          overlay.classList.remove("tsa-visible");
        }
      }, { passive: true });
    }
    var initBtn = document.getElementById("tsa-init-btn"); if (initBtn) initBtn.addEventListener("click", loadData);
  }

  var _uiCreated = false;
  function createUIOnce() {
    if (_uiCreated) return;
    _uiCreated = true;
    createUI();
    // Silently prefetch owned stock data on load so benefit lock, swing shares
    // label, and all sell guards work immediately without opening the TSA panel.
    (function prefetchOwnedMap() {
      var key = getTornKey();
      if (!key || key === "###PDA-APIKEY###") return;
      fetchJSON("https://api.torn.com/user/?selections=stocks&key=" + key)
        .then(function(tornData) {
          if (!tornData || tornData.error) return;
          var map = buildOwnedMap(tornData);
          enrichOwnedMap(map, null);
          lastOwnedMap = map;
          qtUpdateExec(); // refresh swing shares label in QT bar
        })
        .catch(function() {});
    })();
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", createUIOnce);
  } else if (document.readyState === "interactive") {
    window.addEventListener("load", createUIOnce);
  } else {
    createUIOnce();
  }
})();