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.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Torn Stock Analyzer
// @namespace    https://greasyfork.org
// @version      1.0.1
// @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 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 = JSON.parse(localStorage.getItem("tsa_roi_skipped") || "[]");
  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=" + 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) {
            // 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;
    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;
  }

  // Calculate total weekly income from active benefit blocks
  // 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",""));
        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) {
    // Calculate swing capital
    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;
      // Only include if the stock is categorised as a swing trade
      // i.e. no benefit block OR swing override
      var isSwingCategory = swingOverrides.includes(sym) ||
        (!benefitOverrides.includes(sym) && (!o.has_dividend || o.swing_shares > 0) && !o.has_dividend);
      // If the stock has a dividend (benefit block) — only include pure excess swing shares
      // that have been moved to the swing trades section 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 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",""));
      // 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 recommended purchase (not owned, not skipped)
    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 best bridgebuilder: cheapest we can afford that increases income
    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 must have lower cost than target
        if (nextEntry && be.cost >= nextEntry.cost) continue;
        bridgeEntry = be;
        break;
      }
    }

    // Calculate days to goal
    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);
    }

    // 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:"#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 section
    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)");

      // Show 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",""));
          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>';
    }

    // Table 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>';

    // Rows
    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>';

    // Fetch 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 with updated time
          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;
      // Use Torn API's increment field — it is the active/confirmed 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 if: has dividend AND no swing override, OR 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 = [];

    // ── 1. DROP FROM PEAK (max 25p) ───────────────────────────────────
    // Use ALL intervals to find peak — more data = better accuracy
    var peakPrices = [p_h4, p_h8, p_h12, p_d1, p_d2, p_d4, p_w1, p_w2, p_w4, p_n1, p_n2].filter(function(x){ return x > 0; });
    if (peakPrices.length > 0) {
      var peak = Math.max.apply(null, peakPrices);
      var dropPct = ((p_live - peak) / peak * 100);
      if (dropPct <= -3.0)      { score += 25; reasons.push("Drop " + dropPct.toFixed(1) + "%"); }
      else if (dropPct <= -2.0) { score += 20; reasons.push("Drop " + dropPct.toFixed(1) + "%"); }
      else if (dropPct <= -1.0) { score += 14; reasons.push("Drop " + dropPct.toFixed(1) + "%"); }
      else if (dropPct <= -0.5) { score += 8;  reasons.push("Drop " + dropPct.toFixed(1) + "%"); }
      else if (dropPct <= -0.2) { score += 3;  reasons.push("Drop " + dropPct.toFixed(1) + "%"); }
      else                        reasons.push("Drop " + dropPct.toFixed(1) + "%");
    }

    // ── 2. SHORT TREND (max 25p) ──────────────────────────────────────
    // h1 → m30 → m15 → m5 → m1 → live — is price starting to turn up?
    var shortTrendPrices = [p_h1, p_m30, p_m15, p_m5, p_m1, p_live].filter(function(x){ return x > 0; });
    if (shortTrendPrices.length >= 3) {
      var stFirst = shortTrendPrices[0];
      var stLast  = shortTrendPrices[shortTrendPrices.length - 1];
      var stMid   = shortTrendPrices[Math.floor(shortTrendPrices.length / 2)];
      var stTotal = ((stLast - stFirst) / stFirst * 100);
      var stRecent = ((stLast - stMid) / stMid * 100);
      if (stTotal >= 0.3)        { score += 25; reasons.push("Short +" + stTotal.toFixed(2) + "%"); }
      else if (stTotal >= 0.1)   { score += 18; reasons.push("Short +" + stTotal.toFixed(2) + "%"); }
      else if (stTotal >= 0.0)   { score += 10; reasons.push("Short flat"); }
      else if (stTotal >= -0.1)  { score += 5;  reasons.push("Short " + stTotal.toFixed(2) + "%"); }
      else if (stRecent >= 0.05) { score += 8;  reasons.push("Short turning " + stRecent.toFixed(2) + "%"); }
      else                         reasons.push("Short " + stTotal.toFixed(2) + "%");
    }

    // ── 3. INVESTOR FLOW (max 20p) ────────────────────────────────────
    // Rising investors + falling price = smart money buying in
    var invDiffD1 = inv_now - inv_d1;
    var invDiffW1 = inv_now - inv_w1;
    var priceFallingD1 = p_d1 > 0 && p_live < p_d1;
    if (invDiffD1 >= 50 && priceFallingD1)       { score += 20; reasons.push("Inv +" + invDiffD1 + " + price down"); }
    else if (invDiffD1 >= 50)                    { score += 14; reasons.push("Inv +" + invDiffD1 + "/d"); }
    else if (invDiffD1 >= 20 && priceFallingD1)  { score += 16; reasons.push("Inv +" + invDiffD1 + " + price down"); }
    else if (invDiffD1 >= 20)                    { score += 10; reasons.push("Inv +" + invDiffD1 + "/d"); }
    else if (invDiffD1 >= 0)                     { score += 4;  reasons.push("Inv +" + invDiffD1 + "/d"); }
    else if (invDiffW1 >= 100 && priceFallingD1) { score += 12; reasons.push("Inv +" + invDiffW1 + "/w + price down"); }
    else if (invDiffW1 >= 100)                   { score += 6;  reasons.push("Inv +" + invDiffW1 + "/w"); }
    else                                         { score -= 5;  reasons.push("Inv " + invDiffD1 + "/d"); }

    // ── 4. PRICE POSITION IN RANGE (max 25p) ─────────────────────────
    // Use ALL available intervals for range — lower position = better
    var rangePrices = [p_h4, p_h8, p_h12, p_d1, p_d2, p_d4, p_w1, p_w2, p_w4].filter(function(x){ return x > 0; });
    if (rangePrices.length >= 2) {
      var rangeLow  = Math.min.apply(null, rangePrices);
      var rangeHigh = Math.max.apply(null, rangePrices);
      if (rangeHigh > rangeLow) {
        var pos = ((p_live - rangeLow) / (rangeHigh - rangeLow) * 100);
        if (pos <= -20)     { score += 25; reasons.push("Pos " + pos.toFixed(0) + "% EXT LOW"); }
        else if (pos <= 0)  { score += 22; reasons.push("Pos " + pos.toFixed(0) + "%"); }
        else if (pos <= 15) { score += 18; reasons.push("Pos " + pos.toFixed(0) + "%"); }
        else if (pos <= 30) { score += 12; reasons.push("Pos " + pos.toFixed(0) + "%"); }
        else if (pos <= 50) { score += 6;  reasons.push("Pos " + pos.toFixed(0) + "%"); }
        else                  reasons.push("Pos " + pos.toFixed(0) + "%");
      }
    }

    // ── 5. MONTHLY LEVEL (max +5 / -15p) ─────────────────────────────
    // Is price below its monthly average?
    var monthlyRef = p_n1 > 0 ? p_n1 : (p_n2 > 0 ? p_n2 : 0);
    if (monthlyRef > 0) {
      if (p_live >= monthlyRef && p_live >= p_w1) { score -= 15; reasons.push("Above n1+w1"); }
      else if (p_live >= monthlyRef)              { score -= 10; reasons.push("Above n1"); }
      else                                        { score += 5;  reasons.push("Below n1"); }
    }

    // ── HARD FILTERS ──────────────────────────────────────────────────
    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;

    // 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,
      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 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]);

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

      // If ROI planner is active, show it instead
      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;          // Already rallied too much
        if (s.priceAboveWeek) return false;          // Price higher than 1 week ago
        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;
      }

      // 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:"#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 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; }

        // 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 ? "#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 all available 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 content with new colours
      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();
  }
})();