Torn Stock Analyzer

Live buy/sell signal analysis for all 35 Torn City stocks. Shows scoring based on 19 price intervals from Tornsy, automatically splits holdings into swing trades and benefit blocks, and displays profit/loss per position. Only active on the Torn City stock market page. Requires a Full Access Torn API key via Torn PDA.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Torn Stock Analyzer
// @namespace    https://greasyfork.org
// @version      1.0.0
// @author       AeC3
// @description  Live buy/sell signal analysis for all 35 Torn City stocks. Shows scoring based on 19 price intervals from Tornsy, automatically splits holdings into swing trades and benefit blocks, and displays profit/loss per position. Only active on the Torn City stock market page. Requires a Full Access Torn API key via Torn PDA.
// @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
// ==/UserScript==

(function () {
  var TORN_API_KEY = localStorage.getItem("tsa-torn-apikey") || "###PDA-APIKEY###";
  var roiPlannerActive = false;
  var lastOwnedMap = null;
  var lastRaw = null;

  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"
  };

  // 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      width: 340px; 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: #999; 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: #888; 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: #999; 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-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: #888; }\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: #aaa; }\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: #aaa; font-size: 12px; }\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: #555; }\n\n    #tsa-overlay.tsa-dark .tsa-stat-value { color: #e0e0ff; }\n\n    #tsa-overlay.tsa-dark .tsa-section-title { color: #555; }\n\n    #tsa-overlay.tsa-dark .tsa-detail { color: #555; }\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-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: #444; }\n\n    #tsa-overlay.tsa-dark .tsa-loading { color: #444; }\n\n    #tsa-overlay.tsa-dark .tsa-close { color: #555; }\n\n    #tsa-overlay.tsa-dark .tsa-close:hover { color: #aaa; }\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}
  ];

  // Item IDs der har sælgbar markedsværdi
  var ITEM_IDS = [364, 365, 366, 367, 368, 369, 370, 817, 818];
  // PTS giver 100 points = $3M fast
  var PTS_VALUE = 3000000;

  var roiSkipped = JSON.parse(localStorage.getItem("tsa_roi_skipped") || "[]");
  var itemPrices = {}; // cache: itemId -> pris

  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=" + TORN_API_KEY;
    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) {
            // Brug laveste pris
            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;
    ITEM_IDS.forEach(function(id) {
      fetchItemPrice(id, function() {
        remaining--;
        if (remaining === 0) cb();
      });
    });
  }

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

  // Beregn total ugentlig indkomst fra aktive benefit blocks
  // ownedMap: fra buildOwnedMap, raw: fra tornsy for live priser
  function calcWeeklyIncome(ownedMap, raw, extraEntry) {
    var weeklyTotal = 0;
    // Gå igennem alle ejede aktier med 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 tabel entries
      ROI_TABLE.forEach(function(entry) {
        if (entry.sym !== sym) return;
        var tierNum = parseInt(entry.tier.replace("T",""));
        if (tierNum !== increments) return;
        // Payout per 7 dage
        var payoutPerDay = entry.payout / entry.freq;
        var itemVal = getItemValue(entry);
        var itemPerDay = itemVal / entry.freq;
        weeklyTotal += (payoutPerDay + itemPerDay) * 7;
      });
    });
    // Tilføj ekstra 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;
  }

  // Beregn dage til man kan købe target med given ugentlig indkomst + kapital
  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) {
    // Beregn swing kapital
    var swingCapital = 0;
    var swingDetails = [];
    var swingOverrides = JSON.parse(localStorage.getItem("tsa_swing_overrides") || "[]");
    var benefitOverrides = JSON.parse(localStorage.getItem("tsa_benefit_overrides") || "[]");

    Object.keys(ownedMap).forEach(function(sym) {
      var o = ownedMap[sym];
      if (o.swing_shares <= 0) return;
      // Kun medregn hvis aktien er kategoriseret som swing trade
      // dvs. ingen benefit block ELLER swing override
      var isSwingCategory = swingOverrides.includes(sym) ||
        (!benefitOverrides.includes(sym) && (!o.has_dividend || o.swing_shares > 0) && !o.has_dividend);
      // Hvis aktien har dividend (benefit block) — kun medregn rene overskydende swing shares
      // som er placeret i swing trades sektionen via override
      if (o.has_dividend && !swingOverrides.includes(sym)) return;
      if (benefitOverrides.includes(sym)) 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;

    // Find ejede benefit blocks fra ROI tabellen
    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",""));
      // Brug Torn's eget increment felt
      return tierNum <= o.dividend_increment;
    });

    // Ugentlig indkomst fra nuværende benefit blocks
    var weeklyIncome = calcWeeklyIncome(ownedMap, raw, null);

    // Find næste anbefalede køb (ikke ejet, ikke skippet)
    var ownedKeys = ownedEntries.map(function(e){ return e.sym + e.tier; });
    var nextEntry = null;
    for (var i = 0; i < ROI_TABLE.length; i++) {
      var e = ROI_TABLE[i];
      var key = e.sym + e.tier;
      if (ownedKeys.indexOf(key) >= 0) continue;
      if (roiSkipped.indexOf(key) >= 0) continue;
      nextEntry = e;
      break;
    }

    // Find bedste bridgebuilder: billigste vi har råd til der øger indkomst
    var bridgeEntry = null;
    if (nextEntry && totalCapital < nextEntry.cost) {
      for (var j = ROI_TABLE.length - 1; j >= 0; j--) {
        var be = ROI_TABLE[j];
        var bkey = be.sym + be.tier;
        if (ownedKeys.indexOf(bkey) >= 0) continue;
        if (roiSkipped.indexOf(bkey) >= 0) continue;
        if (be.cost > totalCapital) continue;
        if (nextEntry && be.sym === nextEntry.sym && be.tier === nextEntry.tier) continue;
        // Bridgebuilder skal have lavere cost end target
        if (nextEntry && be.cost >= nextEntry.cost) continue;
        bridgeEntry = be;
        break;
      }
    }

    // Beregn dage til mål
    var daysWait = nextEntry ? daysToAfford(nextEntry.cost, totalCapital, weeklyIncome) : 0;
    var daysBridge = Infinity;
    if (bridgeEntry && nextEntry) {
      var incomeWithBridge = calcWeeklyIncome(ownedMap, raw, bridgeEntry);
      var capitalAfterBridge = totalCapital - bridgeEntry.cost;
      daysBridge = daysToAfford(nextEntry.cost, capitalAfterBridge, incomeWithBridge);
    }

    // Byg 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:"#4a4a6a", 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:"#555570", row_border:"#13131f",
      divider:"#1a1a2e", tag_bg:"rgba(120,140,200,0.08)", tag_border:"rgba(120,140,200,0.15)", tag_text:"#6878aa"
    } : {
      bg:"#ffffff", border:"#ddd", bg2:"#f7f9fc", bg3:"#f0f4ff",
      text:"#222", muted:"#888", 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"
    };

    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 (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 sektion
    if (nextEntry) {
      var shortBy = Math.max(0, nextEntry.cost - totalCapital);
      var itemValNext = getItemValue(nextEntry);

      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>';
      html += nmRow("Target", nextEntry.sym + " " + nextEntry.tier + " · " + (nextEntry.payout/nextEntry.cost*(365/nextEntry.freq)*100).toFixed(2) + "% ROI", c.blue);
      html += nmRow("Cost", fmRoi(nextEntry.cost));
      html += nmRow("Available", fmRoi(totalCapital) + (shortBy > 0 ? " · short " + fmRoi(shortBy) : " ✓"), shortBy > 0 ? c.red : c.green);

      // Bridgebuilder
      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>';
      if (bridgeEntry) {
        var itemValBridge = getItemValue(bridgeEntry);
        var incomeWithBridge = calcWeeklyIncome(ownedMap, raw, bridgeEntry);
        html += nmRow("Buy", bridgeEntry.sym + " " + bridgeEntry.tier + " · " + fmRoi(bridgeEntry.cost), c.blue);
        html += nmRow("Income", "+" + fmRoi((bridgeEntry.payout + itemValBridge)/bridgeEntry.freq*7) + " / 7d → " + fmRoi(incomeWithBridge) + " / 7d total");
        html += nmRow("Goal in", daysBridge === Infinity ? "N/A" : "~" + daysBridge + " days", daysBridge < daysWait ? c.green : c.red);
      } else {
        html += nmRow("", "No affordable bridgebuilder", 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)");

      // Vis breakdown af income
      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",""));
          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", daysWait > daysBridge ? c.red : c.green);
      html += '</div>';
    }

    // Tabel header
    html += '<div style="display:grid;grid-template-columns:42px 26px 1fr 54px 24px;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 + '">' +
      '<span>Stock</span><span>Tier</span><span>Cost / Payout</span><span style="text-align:right">ROI</span><span></span></div>';

    // Rækker
    ROI_TABLE.forEach(function(entry, idx) {
      var key = entry.sym + entry.tier;
      var ownedEntry = ownedEntries.find(function(e){ return e.sym === entry.sym && e.tier === entry.tier; });
      var isOwned = !!ownedEntry;
      var isSkipped = roiSkipped.indexOf(key) >= 0;
      var isNext = nextEntry && entry.sym === nextEntry.sym && entry.tier === nextEntry.tier;

      var rowBg = isOwned ? c.owned_bg : isNext ? c.next_bg : isSkipped ? c.skip_bg : "transparent";
      var borderLeft = isOwned ? c.owned_border : isNext ? c.next_border : isSkipped ? c.skip_border : "transparent";
      var symColor = isOwned ? c.green : isNext ? c.blue : isSkipped ? c.red : c.neutral;
      var roiPct = (entry.payout / entry.cost * (365/entry.freq) * 100).toFixed(2) + "%";

      var itemVal = getItemValue(entry);
      var payoutStr = fmRoi(entry.payout) + (itemVal > 0 ? " + " + fmRoi(itemVal) : "") + " / " + entry.freq + "d";

      var skipBtnStyle = 'width:20px;height:20px;border-radius:50%;border:1px solid ' + c.divider + ';background:none;cursor:pointer;font-size:9px;color:' + c.muted + ';display:flex;align-items:center;justify-content:center;justify-self:center;' + (isOwned ? 'opacity:0.2;pointer-events:none;' : '');
      var skipLabel = isSkipped ? "↩" : "✕";

      html += '<div data-roi-key="' + key + '" style="display:grid;grid-template-columns:42px 26px 1fr 54px 24px;gap:4px;align-items:center;padding:7px 14px;border-bottom:1px solid ' + c.row_border + ';background:' + rowBg + ';border-left:2px solid ' + borderLeft + ';cursor:default">' +
        '<span style="' + s + ';font-weight:700;font-size:12px;color:' + symColor + '">' + entry.sym + '</span>' +
        '<span style="' + s + ';font-size:9px;color:' + c.muted + '">' + entry.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">' + fmRoi(entry.cost) + '</span>' +
          '<span style="font-size:9px;color:' + c.muted + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + payoutStr + '</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="' + (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 ? '#555' : '#aaa') + ';font-size:12px">Loading item prices...</div>';

    // Hent cash balance
    GM_xmlhttpRequest({
      method: "GET",
      url: "https://api.torn.com/user/?selections=money&key=" + TORN_API_KEY,
      onload: function(r) {
        var cashBalance = 0;
        try {
          var d = JSON.parse(r.responseText);
          cashBalance = d.money_onhand || 0;
        } catch(e) {}

        fetchAllItemPrices(function() {
          var html = renderROIPlanner(ownedMap, raw, cashBalance);
          content.innerHTML = html;

          // Skip/unskip event listeners
          content.querySelectorAll(".tsa-roi-skip").forEach(function(btn) {
            btn.addEventListener("click", function(e) {
              e.stopPropagation();
              if (btn.dataset.owned === "1") return;
              var key = btn.dataset.key;
              var idx = roiSkipped.indexOf(key);
              if (idx >= 0) roiSkipped.splice(idx, 1);
              else roiSkipped.push(key);
              localStorage.setItem("tsa_roi_skipped", JSON.stringify(roiSkipped));
              showROIPlanner(ownedMap, raw);
            });
          });

          // Footer med updated tid
          content.insertAdjacentHTML("beforeend",
            '</div>' +
          '<div style="padding:7px 14px;display:flex;justify-content:space-between;align-items:center;border-top:1px solid ' + c.divider + ';background:' + c.bg + '">' +
            '<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>'
          );
        });
      },
      onerror: function() {
        fetchAllItemPrices(function() {
          var html = renderROIPlanner(ownedMap, raw, 0);
          content.innerHTML = html;
          content.querySelectorAll(".tsa-roi-skip").forEach(function(btn) {
            btn.addEventListener("click", function(e) {
              e.stopPropagation();
              if (btn.dataset.owned === "1") return;
              var key = btn.dataset.key;
              var idx = roiSkipped.indexOf(key);
              if (idx >= 0) roiSkipped.splice(idx, 1);
              else roiSkipped.push(key);
              localStorage.setItem("tsa_roi_skipped", JSON.stringify(roiSkipped));
              showROIPlanner(ownedMap, raw);
            });
          });
        });
      }
    });
  }

  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)];
      if (!acronym) return;
      var transactions = s.transactions ? Object.keys(s.transactions).map(function(k) { return s.transactions[k]; }) : [];
      if (transactions.length === 0) return;
      var totalShares = 0, totalCost = 0, earliestTime = Infinity;
      transactions.forEach(function(t) {
        totalShares += t.shares || 0;
        totalCost += (t.shares || 0) * (t.bought_price || 0);
        if (t.time_bought && t.time_bought < earliestTime) earliestTime = t.time_bought;
      });

      var req = BENEFIT_REQ[acronym] || 0;
      // Brug Torn API's increment felt — det er den aktive/bekræftede tier
      var apiIncrement = (s.dividend && s.dividend.increment) || 0;
      var benefitShares = apiIncrement * req;
      var swingShares = totalShares - benefitShares;

      owned[acronym] = {
        shares: totalShares,
        swing_shares: swingShares,
        benefit_shares: benefitShares,
        avg_price: totalShares > 0 ? totalCost / totalShares : 0,
        time_bought: earliestTime === Infinity ? null : earliestTime,
        has_dividend: benefitShares > 0,
        has_swing: swingShares > 0,
        dividend_progress: (s.dividend && s.dividend.progress) || 0,
        dividend_frequency: (s.dividend && s.dividend.frequency) || 0,
        dividend_increment: apiIncrement,
        transactions: transactions.sort(function(a, b) { return b.time_bought - a.time_bought; })
      };
    });
    return owned;
  }

  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, swingOverrides, benefitOverrides) {
    var s = stock.toUpperCase();
    var r = raw ? raw.find(function(x) { return x.stock === s; }) : null;
    if (!r) return null;

    var owned = ownedMap[s];
    // Benefit block hvis: har dividend OG ikke swing override, ELLER benefit override
    var isInBenefitCategory = owned &&
      ((owned.has_dividend && !(swingOverrides || []).includes(s)) ||
       (benefitOverrides || []).includes(s));
    var p_live = parseFloat(r.price) || 0;
    var p_m1  = parseFloat((r.interval && r.interval.m1 && r.interval.m1.price))  || 0;
    var p_m5  = parseFloat((r.interval && r.interval.m5 && r.interval.m5.price))  || 0;
    var p_m15 = parseFloat((r.interval && r.interval.m15 && r.interval.m15.price)) || 0;
    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_h4  = parseFloat((r.interval && r.interval.h4 && r.interval.h4.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_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_d4  = parseFloat((r.interval && r.interval.d4 && r.interval.d4.price))  || 0;
    var p_w1  = parseFloat((r.interval && r.interval.w1 && r.interval.w1.price))  || 0;
    var p_w2  = parseFloat((r.interval && r.interval.w2 && r.interval.w2.price))  || 0;
    var p_w4  = parseFloat((r.interval && r.interval.w4 && r.interval.w4.price))  || 0;
    var p_n1  = parseFloat((r.interval && r.interval.n1 && r.interval.n1.price))  || 0;
    var p_n2  = parseFloat((r.interval && r.interval.n2 && r.interval.n2.price))  || 0;
    var inv_now = r.investors || 0;
    var inv_d1  = (r.interval && r.interval.d1 && r.interval.d1.investors) || 0;
    var inv_w1  = (r.interval && r.interval.w1 && r.interval.w1.investors) || 0;

    var score = 0;
    var reasons = [];

    // Saml alle tilgængelige priser i kronologisk rækkefølge (ældst → nyest)
    var allPrices = [p_n2, p_n1, p_w4, p_w2, p_w1, p_d4, p_d2, p_d1, p_h12, p_h8, p_h4, p_h2, p_h1, p_m30, p_m15, p_m5, p_m1, p_live].filter(function(x) { return x > 0; });

    // Spredte priser til RSI og BB — undgår at m1/m5/m15 dominerer
    var spreadPrices = [p_n2, p_n1, p_w4, p_w2, p_w1, p_d4, p_d2, p_d1, p_h12, p_h8, p_h4, p_h2, p_h1, p_live].filter(function(x) { return x > 0; });

    // ── VENDEPUNKT CHECK ─────────────────────────────────────────────────
    // Vi vil have aktier der ER lave og ENDNU ikke steget
    // Ikke aktier der allerede er på vej op
    var microUp = p_h1 > 0 && p_live > p_h1;
    var shortTurning = (p_h2 > 0 && p_h1 > p_h2) || (p_h4 > 0 && p_h1 > p_h4);
    var recentLow = Math.min.apply(null, [p_w1, p_d2, p_d1, p_h12, p_h8, p_h4, p_h2, p_h1].filter(function(x){ return x > 0; }));
    var alreadyRallied = recentLow > 0 && ((p_live - recentLow) / recentLow * 100) > 0.3;

    // Straf for opadgående trend — vi vil købe INDEN den vender
    var turnScore = 0;
    if (alreadyRallied) {
      turnScore = -15; reasons.push("Already rallied ✗");
    } else if (microUp && shortTurning) {
      turnScore = -8; reasons.push("Already turning up");
    } else if (!microUp && !shortTurning) {
      turnScore = 10; reasons.push("Still at bottom ✓");
    } else {
      turnScore = 0; reasons.push("Flat");
    }
    score += turnScore;
    var rsiScore = 0, rsiVal = 50;
    if (spreadPrices.length >= 5) {
      var rsiPrices = spreadPrices.slice(-14);
      var gains = 0, losses = 0, rsiCount = rsiPrices.length - 1;
      for (var ri = 0; ri < rsiCount; ri++) {
        var chg = rsiPrices[ri+1] - rsiPrices[ri];
        if (chg > 0) gains += chg; else losses += Math.abs(chg);
      }
      var avgGain = gains / rsiCount, avgLoss = losses / rsiCount;
      rsiVal = avgLoss === 0 ? 100 : 100 - (100 / (1 + avgGain / avgLoss));
      if (rsiVal <= 25)      { rsiScore = 25; reasons.push("RSI " + rsiVal.toFixed(0) + " (oversold)"); }
      else if (rsiVal <= 35) { rsiScore = 18; reasons.push("RSI " + rsiVal.toFixed(0)); }
      else if (rsiVal <= 45) { rsiScore = 10; reasons.push("RSI " + rsiVal.toFixed(0)); }
      else if (rsiVal >= 70) { rsiScore = -10; reasons.push("RSI " + rsiVal.toFixed(0) + " (overbought)"); }
      else                   reasons.push("RSI " + rsiVal.toFixed(0));
    }
    score += rsiScore;

    // ── 2. BOLLINGER BANDS (max 25p) ──────────────────────────────────
    var bbScore = 0;
    if (spreadPrices.length >= 5) {
      var bbMean = spreadPrices.reduce(function(a,b){ return a+b; }, 0) / spreadPrices.length;
      var bbVariance = spreadPrices.reduce(function(sum, p){ return sum + Math.pow(p - bbMean, 2); }, 0) / spreadPrices.length;
      var bbStd = Math.sqrt(bbVariance);
      var bbLower = bbMean - 2 * bbStd;
      var bbUpper = bbMean + 2 * bbStd;
      var bbPos = bbStd > 0 ? ((p_live - bbLower) / (bbUpper - bbLower) * 100) : 50;
      if (p_live <= bbLower)        { bbScore = 25; reasons.push("BB below lower"); }
      else if (bbPos <= 15)         { bbScore = 18; reasons.push("BB " + bbPos.toFixed(0) + "%"); }
      else if (bbPos <= 30)         { bbScore = 10; reasons.push("BB " + bbPos.toFixed(0) + "%"); }
      else if (bbPos <= 50)         { bbScore = 5;  reasons.push("BB " + bbPos.toFixed(0) + "%"); }
      else if (p_live >= bbUpper)   { bbScore = -10; reasons.push("BB above upper"); }
      else                          reasons.push("BB " + bbPos.toFixed(0) + "%");
    }
    score += bbScore;

    // ── 3. MA CROSSOVER (max 20p) ─────────────────────────────────────
    // Kort MA = m1,m5,m15,m30,h1 / Lang MA = d1,d2,d4,w1,w2
    var maScore = 0;
    var shortPrices = [p_m1, p_m5, p_m15, p_m30, p_h1].filter(function(x){ return x > 0; });
    var longPrices  = [p_d1, p_d2, p_d4, p_w1, p_w2].filter(function(x){ return x > 0; });
    if (shortPrices.length >= 2 && longPrices.length >= 2) {
      var shortMA = shortPrices.reduce(function(a,b){ return a+b; },0) / shortPrices.length;
      var longMA  = longPrices.reduce(function(a,b){ return a+b; },0) / longPrices.length;
      var maDiff = ((shortMA - longMA) / longMA * 100);
      if (maDiff >= 0.3)       { maScore = 20; reasons.push("MA cross +" + maDiff.toFixed(2) + "%"); }
      else if (maDiff >= 0.05) { maScore = 13; reasons.push("MA cross +" + maDiff.toFixed(2) + "%"); }
      else if (maDiff >= -0.3) { maScore = 8;  reasons.push("MA near cross " + maDiff.toFixed(2) + "%"); }
      else if (maDiff >= -1.0) { maScore = 4;  reasons.push("MA cross " + maDiff.toFixed(2) + "%"); }
      else                     { maScore = 0;  reasons.push("MA bearish " + maDiff.toFixed(2) + "%"); }
    }
    score += maScore;

    // ── 4. PRICE vs LONG MA (max 15p) ────────────────────────────────
    // Hvor langt er live prisen under lang MA
    var pvmaScore = 0;
    if (longPrices.length >= 2) {
      var lMA = longPrices.reduce(function(a,b){ return a+b; },0) / longPrices.length;
      var pvmaDiff = ((p_live - lMA) / lMA * 100);
      if (pvmaDiff <= -2.0)      { pvmaScore = 15; reasons.push("Price -" + Math.abs(pvmaDiff).toFixed(1) + "% vs MA"); }
      else if (pvmaDiff <= -1.0) { pvmaScore = 10; reasons.push("Price -" + Math.abs(pvmaDiff).toFixed(1) + "% vs MA"); }
      else if (pvmaDiff <= -0.3) { pvmaScore = 6;  reasons.push("Price -" + Math.abs(pvmaDiff).toFixed(1) + "% vs MA"); }
      else if (pvmaDiff <= 0.3)  { pvmaScore = 3;  reasons.push("Price near MA"); }
      else if (pvmaDiff >= 2.0)  { pvmaScore = 0;  reasons.push("Price +" + pvmaDiff.toFixed(1) + "% vs MA"); }
      else                       { pvmaScore = 1;  reasons.push("Price " + pvmaDiff.toFixed(1) + "% vs MA"); }
    }
    score += pvmaScore;

    // ── 5. INVESTOR MOMENTUM (max 15p) ───────────────────────────────
    // Stigende investors + faldende pris = smart money køber
    var invScore = 0;
    var invDiffD1 = inv_now - inv_d1;
    var invDiffW1 = inv_now - inv_w1;
    var priceFallingD1 = p_d1 > 0 && p_live < p_d1;
    if (invDiffD1 >= 30 && priceFallingD1)  { invScore = 15; reasons.push("Inv +" + invDiffD1 + " + price down"); }
    else if (invDiffD1 >= 30)               { invScore = 10; reasons.push("Inv +" + invDiffD1 + "/d"); }
    else if (invDiffD1 >= 10)               { invScore = 6;  reasons.push("Inv +" + invDiffD1 + "/d"); }
    else if (invDiffD1 >= 0)                { invScore = 2;  reasons.push("Inv +" + invDiffD1 + "/d"); }
    else if (invDiffW1 >= 50 && priceFallingD1) { invScore = 8; reasons.push("Inv +" + invDiffW1 + "/w + price down"); }
    else if (invDiffW1 >= 50)               { invScore = 4;  reasons.push("Inv +" + invDiffW1 + "/w"); }
    else                                    reasons.push("Inv " + invDiffD1 + "/d");
    score += invScore;

    // SIGNAL
    var signal;
    if (score >= 65)      signal = "STRONG BUY";
    else if (score >= 50) signal = "BUY";
    else if (score >= 35) signal = "CONSIDER";
    else                  signal = "WAIT";

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

    // Calculate profit % for ALL owned stocks
    if (owned && owned.avg_price > 0) {
      netProfitPct = ((p_live - 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 netProfitPctWithFee = ((p_live * 0.999 - owned.avg_price) / owned.avg_price * 100);
      if (netProfitPct >= 0.3)              sellSignal = "SELL (PROFIT)";
      else if (netProfitPctWithFee <= -1.0) sellSignal = "SELL (STOP LOSS)";
    }

    return {
      symbol: s, score, signal, sellSignal,
      owned: !!owned,
      alreadyRallied: alreadyRallied,
      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 loadData() {
    var content = document.getElementById("tsa-content");
    var isDarkMode = document.getElementById("tsa-overlay").classList.contains("tsa-dark");
    content.style.background = isDarkMode ? "#0f0f1a" : "#ffffff";
    content.innerHTML = "<div class=\"tsa-loading\">Fetching data...</div>";

    Promise.all([
      fetchJSON("https://api.torn.com/user/?selections=stocks&key=" + TORN_API_KEY),
      fetchJSON("https://tornsy.com/api/stocks?interval=m1,m5,m15,m30,h1"),
      fetchJSON("https://tornsy.com/api/stocks?interval=h2,h4,h8,h12,d1"),
      fetchJSON("https://tornsy.com/api/stocks?interval=d2,d4,w1,w2,w4"),
      fetchJSON("https://tornsy.com/api/stocks?interval=n1,n2")
    ]).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("Torn API: " + tornData.error.error); }

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

      // Gem til ROI planner
      lastOwnedMap = ownedMap;
      lastRaw = raw;

      // Hvis ROI planner er aktiv, vis den i stedet
      if (roiPlannerActive) { showROIPlanner(ownedMap, raw); return; }
      var hiddenStocks = JSON.parse(localStorage.getItem("tsa_hidden") || "[]");
      var benefitOverrides = JSON.parse(localStorage.getItem("tsa_benefit_overrides") || "[]");
      var swingOverrides = JSON.parse(localStorage.getItem("tsa_swing_overrides") || "[]");

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

      var top5BuyAll = stockResults.filter(function(s) {
        if (s.score < 35) return false;
        if (s.alreadyRallied) return false;          // Allerede steget for meget
        if (!s.owned) return true;
        if (!s.has_swing && s.has_benefit) return true;
        return false;
      });
      var top5Buy = top5BuyAll.slice(0, 5);

      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];
        if (ownedEntry.has_dividend) continue;
        var rawEntry = raw ? raw.find(function(x) { return x.stock === profitSym; }) : null;
        if (!rawEntry || !ownedEntry.avg_price) continue;
        totalProfit += parseFloat(rawEntry.price) * ownedEntry.shares - ownedEntry.avg_price * ownedEntry.shares;
      }

      // Farvepalette baseret på 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:"#4a4a6a", 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>" +
        "<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>";

      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;display:flex;justify-content:space-between;align-items:center\">" +
          "Top " + top5Buy.length + " buy" +
          "<button class=\"tsa-refresh\" id=\"tsa-refresh-btn\" style=\"font-size:10px;padding:3px 10px\">↻ Update</button></div>";
        top5Buy.forEach(function(s) {
          var chartId = "tsa-chart-" + s.symbol;
          html += "<div style=\"margin-bottom:5px\">" +
            "<div class=\"tsa-buy-row\" data-symbol=\"" + s.symbol + "\" data-chart=\"" + chartId + "\" style=\"display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-radius:8px;cursor:pointer;background:" + d.rowBuy + ";border:1px solid " + d.rowBuyBorder + "\">" +
            "<div style=\"display:flex;flex-direction:column;gap:2px\">" +
            "<span style=\"font-size:13px;font-weight:bold;color:" + d.green + ";" + 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:" + d.green + ";" + ms + "\">" + s.score + "</span>" +
            "<span style=\"font-size:9px;color:" + d.green + ";font-weight:bold\">" + s.signal + "</span>" +
            "</div></div>" +
            "<div id=\"" + chartId + "\" style=\"display:none;padding:8px 10px;background:" + d.bg2 + ";border:1px solid " + d.border + ";border-radius:0 0 8px 8px;margin-top:-4px\">" +
            "<div style=\"font-size:9px;color:" + d.muted + ";margin-bottom:6px;text-transform:uppercase;letter-spacing:0.08em;" + ms + "\">Price history · " + s.symbol + "</div>" +
            "<canvas id=\"canvas-" + s.symbol + "\" width=\"280\" height=\"80\" style=\"width:100%;height:80px\"></canvas>" +
            "<div style=\"display:flex;justify-content:space-between;font-size:8px;color:" + d.muted + ";margin-top:3px;" + ms + "\"><span>1W ago</span><span>4D</span><span>1D</span><span>Now</span></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;display:flex;justify-content:space-between;align-items:center\">" +
          "Buy signals<button class=\"tsa-refresh\" id=\"tsa-refresh-btn\" style=\"font-size:10px;padding:3px 10px\">↻ Update</button></div>" +
          "<div style=\"color:" + d.muted + ";font-size:11px;padding:8px 0\">No signals right now</div></div>";
      }

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

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

      var isSwingCategory = function(checkSym) {
        var o = ownedMap[checkSym];
        if (!o) return false;
        if (swingOverrides.includes(checkSym)) return true;
        if (benefitOverrides.includes(checkSym)) return false;
        return !o.has_dividend || o.swing_shares > 0;
      };

      var swingTrades = allOwned.filter(function(s) { return isSwingCategory(s.symbol); });
      var benefitBlocks = allOwned.filter(function(s) { return isBenefitCategory(s.symbol); });

      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);
        var sharesStr = displayShares > 0 ? displayShares.toLocaleString("en-US") + " shares" : "?";
        var pct = s.netProfitPct !== null ? (s.netProfitPct >= 0 ? "+" : "") + s.netProfitPct.toFixed(2) + "%" : "?%";
        var isProfit = (s.netProfitPct || 0) >= 0;
        var col = isBenefit ? 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 moveBg = isBenefit ? d.moveBg : d.moveBg2;
        var moveBorder = isBenefit ? d.moveBorder : d.moveBorder2;
        var moveColor = isBenefit ? d.moveColor : d.moveColor2;
        var moveTitle = isBenefit ? "Move to Swing trades" : "Move to Benefit blocks";

        var txHtml = "";
        if (s.transactions && s.transactions.length > 0) {
          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 txCol = txProfit >= 0 ? "#1a8a45" : "#cc2222";
            var txColD = txProfit >= 0 ? d.green : d.red;
            txHtml += "<div style=\"display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid " + d.divider + ";font-size:11px;\">" +
              "<div><span style=\"color:" + d.muted + ";" + ms + "\">Block " + (idx+1) + "</span><span style=\"color:" + d.muted + ";margin-left:6px\">" + txDate + "</span></div>" +
              "<div style=\"text-align:right\"><span style=\"color:" + d.text + ";" + ms + "\">" + txShares + " @ " + txPrice + "</span><br>" +
              "<span style=\"font-weight:bold;color:" + txColD + "\">" + (txProfit >= 0 ? "+" : "") + txPct + "%</span></div></div>";
          });
        }

        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 + "\">" +
          "<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 + " · Score " + s.score + (s.hasDividend ? " · DIV" : "") + "</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>" +
          "<button class=\"tsa-move-btn\" data-symbol=\"" + s.symbol + "\" data-category=\"" + category + "\" title=\"" + moveTitle + "\"" +
          " style=\"flex-shrink:0;width:28px;height:28px;border-radius:6px;border:1px solid " + moveBorder + ";background:" + moveBg + ";cursor:pointer;font-size:12px;color:" + moveColor + ";\">&lt;&gt;</button>" +
          "</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;\">" +
          "<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.toFixed(2) + "</strong> · Live: <strong style=\"color:" + d.text + "\">$" + s.p_live.toFixed(2) + "</strong></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 = localStorage.getItem("tsa_swing_collapsed") === "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 = localStorage.getItem("tsa_benefit_collapsed") === "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>";
        }
      }

      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") + "</span>" +
        "</div>";

      content.innerHTML = html;

      var refreshBtn = document.getElementById("tsa-refresh-btn");
      if (refreshBtn) refreshBtn.addEventListener("click", loadData);

      // 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; }

        // Kronologisk rækkefølge af spredte priser
        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 ? "#4a4a6a" : "#aaa";

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

        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);
      }

      document.querySelectorAll(".tsa-buy-row").forEach(function(row) {
        row.addEventListener("click", function() {
          var sym = row.dataset.symbol;
          var chartId = row.dataset.chart;
          var chartDiv = document.getElementById(chartId);
          if (!chartDiv) return;
          var isOpen = chartDiv.style.display === "block";
          chartDiv.style.display = isOpen ? "none" : "block";
          if (!isOpen) {
            // Merge alle tilgængelige raw data
            drawChart(sym, chartId, raw);
          }
        });
      });

      document.querySelectorAll("[data-detail]").forEach(function(row) {
        row.addEventListener("click", function() {
          var panel = document.getElementById(row.dataset.detail);
          if (panel) panel.style.display = panel.style.display === "none" ? "block" : "none";
        });
      });

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

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

      document.querySelectorAll(".tsa-move-btn").forEach(function(btn) {
        btn.addEventListener("click", function(e) {
          e.stopPropagation();
          var moveSym = btn.dataset.symbol;
          var moveCat = btn.dataset.category;
          var bOvr = JSON.parse(localStorage.getItem("tsa_benefit_overrides") || "[]");
          var sOvr = JSON.parse(localStorage.getItem("tsa_swing_overrides") || "[]");
          if (moveCat === "swing") {
            if (!bOvr.includes(moveSym)) bOvr.push(moveSym);
            var si = sOvr.indexOf(moveSym); if (si !== -1) sOvr.splice(si, 1);
          } else {
            if (!sOvr.includes(moveSym)) sOvr.push(moveSym);
            var bi = bOvr.indexOf(moveSym); if (bi !== -1) bOvr.splice(bi, 1);
          }
          localStorage.setItem("tsa_benefit_overrides", JSON.stringify(bOvr));
          localStorage.setItem("tsa_swing_overrides", JSON.stringify(sOvr));
          loadData();
        });
      });

    }).catch(function(e) {
      content.innerHTML = "<div class=\"tsa-error\">Error: " + 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);
    });
  }

  function createUI() {
    injectStyles();
    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";
    if (localStorage.getItem("tsa_dark") === "true") overlay.classList.add("tsa-dark");
    var isDarkInit = localStorage.getItem("tsa_dark") === "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>" +
        "</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);

    document.getElementById("tsa-theme-btn").addEventListener("click", function() {
      var isDark = overlay.classList.toggle("tsa-dark");
      localStorage.setItem("tsa_dark", isDark.toString());
      document.getElementById("tsa-theme-btn").textContent = isDark ? "[L]" : "[D]";
      // Re-render indhold med nye farver
      if (roiPlannerActive && lastOwnedMap) {
        showROIPlanner(lastOwnedMap, lastRaw);
      } else if (lastOwnedMap) {
        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";
      overlay.style.display = isOpen ? "none" : "block";
      if (!isOpen) loadData();
    });
    document.getElementById("tsa-close").addEventListener("click", function() {
      overlay.style.display = "none";
    });
    var initBtn = document.getElementById("tsa-init-btn"); if (initBtn) initBtn.addEventListener("click", loadData);
  }

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