Greasy Fork is available in English.
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.
// ==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 · ↩ 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 + ";\"><></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 ? "►" : "▼") + "</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 ? "►" : "▼") + "</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();
}
})();