Stock vault for Torn — spread, rebalance, withdraw, P&L, blocks, swing trading. Smart series UI.
// ==UserScript==
// @name Smart Stock Vault
// @namespace smart.torn.tools
// @version 10.2.12
// @description Stock vault for Torn — spread, rebalance, withdraw, P&L, blocks, swing trading. Smart series UI.
// @author Noobler
// @homepageURL https://greasyfork.org/en/scripts/564798-smart-stock-vault
// @supportURL https://www.torn.com/forums.php#/p=threads&f=67&t=16535978&b=0&a=0
// @match https://www.torn.com/page.php?sid=stocks*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @require https://code.jquery.com/jquery-3.7.1.min.js
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @grant unsafeWindow
// @connect tornsy.com
// @run-at document-idle
// @license MIT
// ==/UserScript==
(() => {
// src/shared/theme.js
var tokens = {
accent: "#caa14a",
accentMuted: "rgba(202, 161, 74, 0.35)",
accentSubtle: "rgba(202, 161, 74, 0.12)",
accentText: "#f0e3c0",
panelBg: "linear-gradient(180deg, #23252b, #1b1d22)",
panelHeaderBg: "linear-gradient(180deg, #2c2f37, #23252b)",
panelBorder: "#34373f",
surface: "#15161a",
text: "#d7d9de",
textMuted: "#8a8d96",
textDim: "#71747d",
success: "#6fcf86",
successBg: "#16341f",
successBorder: "#2c6b3c",
warning: "#ffd966",
warningBg: "#332b13",
warningBorder: "#6b5a1f",
danger: "#ff6b6b",
dangerBg: "#341818",
dangerBorder: "#6b2c2c",
info: "#8dbdf0",
infoBg: "#15263d",
infoBorder: "#2c4f7b",
hover: "#d8b25c",
font: "'Trebuchet MS', Verdana, sans-serif",
radius: "8px",
shadow: "0 2px 10px rgba(0, 0, 0, 0.35)",
statDex: "#8a7ff0",
statDef: "#e07a4f",
statStr: "#3fae84",
statSpd: "#4a97e6"
};
var STYLE_ID = "hf-torn-theme";
function injectStyles(scopeClass = "hf-torn") {
if (document.getElementById(STYLE_ID)) {
return;
}
const css = `
.${scopeClass} {
--hf-accent: ${tokens.accent};
--hf-accent-muted: ${tokens.accentMuted};
--hf-accent-subtle: ${tokens.accentSubtle};
--hf-accent-text: ${tokens.accentText};
--hf-panel-bg: ${tokens.panelBg};
--hf-panel-header-bg: ${tokens.panelHeaderBg};
--hf-panel-border: ${tokens.panelBorder};
--hf-surface: ${tokens.surface};
--hf-text: ${tokens.text};
--hf-text-muted: ${tokens.textMuted};
--hf-text-dim: ${tokens.textDim};
--hf-success: ${tokens.success};
--hf-success-bg: ${tokens.successBg};
--hf-success-border: ${tokens.successBorder};
--hf-warning: ${tokens.warning};
--hf-warning-bg: ${tokens.warningBg};
--hf-warning-border: ${tokens.warningBorder};
--hf-danger: ${tokens.danger};
--hf-danger-bg: ${tokens.dangerBg};
--hf-danger-border: ${tokens.dangerBorder};
--hf-info: ${tokens.info};
--hf-info-bg: ${tokens.infoBg};
--hf-info-border: ${tokens.infoBorder};
--hf-hover: ${tokens.hover};
--hf-radius: ${tokens.radius};
--hf-shadow: ${tokens.shadow};
--hf-font: ${tokens.font};
--hf-stat-dex: ${tokens.statDex};
--hf-stat-def: ${tokens.statDef};
--hf-stat-str: ${tokens.statStr};
--hf-stat-spd: ${tokens.statSpd};
color: var(--hf-text);
font-family: var(--hf-font);
font-size: 12px;
line-height: 1.4;
box-sizing: border-box;
}
.${scopeClass} *, .${scopeClass} *::before, .${scopeClass} *::after {
box-sizing: border-box;
}
.${scopeClass} .hf-btn {
appearance: none;
border: 1px solid var(--hf-panel-border);
border-radius: 5px;
background: var(--hf-surface);
color: var(--hf-accent-text);
cursor: pointer;
padding: 6px 10px;
font: inherit;
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.${scopeClass} .hf-btn:hover {
background: var(--hf-accent-subtle);
border-color: var(--hf-accent);
color: #fff;
}
.${scopeClass} .hf-btn:disabled {
opacity: 0.65;
cursor: wait;
pointer-events: none;
}
.${scopeClass} .hf-btn.is-active,
.${scopeClass} .hf-btn--primary {
background: var(--hf-accent);
border-color: var(--hf-accent);
color: #1b1d22;
font-weight: 700;
}
.${scopeClass} .hf-btn.is-active:hover,
.${scopeClass} .hf-btn--primary:hover {
background: var(--hf-hover);
border-color: var(--hf-hover);
}
.${scopeClass} .hf-muted {
color: var(--hf-text-muted);
}
.${scopeClass} .hf-dim {
color: var(--hf-text-dim);
}
.${scopeClass} .hf-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
background: var(--hf-accent);
color: #1b1d22;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.${scopeClass} .hf-input,
.${scopeClass} .hf-select {
width: 100%;
box-sizing: border-box;
padding: 6px 8px;
background: var(--hf-surface);
border: 1px solid var(--hf-panel-border);
color: var(--hf-accent-text);
border-radius: 5px;
font: inherit;
font-size: 12px;
}
.${scopeClass} .hf-label {
display: block;
font-size: 11px;
color: var(--hf-text-muted);
margin: 12px 0 4px;
}
.${scopeClass} .hf-table {
width: 100%;
border-collapse: collapse;
}
.${scopeClass} .hf-table th,
.${scopeClass} .hf-table td {
padding: 4px 6px;
text-align: left;
border-bottom: 1px solid var(--hf-panel-border);
color: var(--hf-text) !important;
background: transparent !important;
}
.${scopeClass} .hf-table th {
font-weight: bold;
color: var(--hf-accent-text) !important;
}
.${scopeClass} .hf-table tbody tr:hover td {
background: var(--hf-accent-subtle) !important;
}
.${scopeClass} .hf-table .hf-pos--1 {
color: #e8c547 !important;
font-weight: 700;
}
.${scopeClass} .hf-table .hf-pos--2 {
color: #c4c9d4 !important;
font-weight: 700;
}
.${scopeClass} .hf-table .hf-pos--3 {
color: #c98a5a !important;
font-weight: 700;
}
.${scopeClass} .hf-racing-log-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.${scopeClass} .hf-racing-log-hint {
font-size: 11px;
color: var(--hf-text-muted);
}
.${scopeClass} strong {
color: var(--hf-accent-text);
}
.${scopeClass} hr {
border: none;
border-top: 1px solid var(--hf-panel-border);
margin: 10px 0;
}
.${scopeClass} .hf-section-break {
box-sizing: border-box;
border-top: 1px solid var(--hf-panel-border);
margin-top: 10px;
padding-top: 12px;
}
.${scopeClass} .hf-guide-row {
margin: 0 0 4px;
}
.${scopeClass} .hf-guide-row:first-child {
margin-top: 0;
}
.${scopeClass} .hf-racing-car-row {
margin-bottom: 10px;
}
.${scopeClass} .hf-racing-live {
margin-bottom: 10px;
padding: 6px 8px;
border-radius: 5px;
background: var(--hf-accent-subtle);
border: 1px solid var(--hf-accent-muted);
color: var(--hf-accent-text);
font-size: 11px;
}
.${scopeClass} .hf-placement-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin: 4px 0 2px;
}
.${scopeClass} .hf-placement-chip {
display: inline-flex;
align-items: center;
gap: 4px;
width: auto;
max-width: none;
flex: 0 0 auto;
padding: 3px 7px 3px 5px;
border-radius: 5px;
border: 1px solid var(--hf-panel-border);
background: var(--hf-surface);
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.${scopeClass} .hf-placement-chip.is-zero {
opacity: 0.35;
}
.${scopeClass} .hf-placement-chip svg.hf-placement-icon {
width: 14px !important;
height: 14px !important;
min-width: 14px !important;
max-width: 14px !important;
min-height: 14px !important;
max-height: 14px !important;
flex: 0 0 14px !important;
display: block !important;
overflow: visible !important;
vertical-align: middle;
}
.${scopeClass} .hf-placement-chip--gold {
color: #e8c547;
border-color: rgba(232, 197, 71, 0.35);
background: rgba(232, 197, 71, 0.1);
}
.${scopeClass} .hf-placement-chip--silver {
color: #c4c9d4;
border-color: rgba(196, 201, 212, 0.35);
background: rgba(196, 201, 212, 0.08);
}
.${scopeClass} .hf-placement-chip--bronze {
color: #c98a5a;
border-color: rgba(201, 138, 90, 0.35);
background: rgba(201, 138, 90, 0.1);
}
.${scopeClass} .hf-placement-chip--out {
color: var(--hf-text-muted);
border-color: var(--hf-panel-border);
background: rgba(0, 0, 0, 0.15);
}
.${scopeClass}.hf-panel {
margin: 0 0 12px;
border-radius: var(--hf-radius);
background: var(--hf-panel-bg);
border: 1px solid var(--hf-panel-border);
box-shadow: var(--hf-shadow);
overflow: hidden;
}
.${scopeClass}.hf-panel--page {
width: 100%;
}
.${scopeClass}.hf-panel--fixed {
position: fixed;
z-index: 9999;
}
.${scopeClass} .hf-panel-header {
background: var(--hf-panel-header-bg);
border-bottom: 1px solid var(--hf-panel-border);
border-radius: var(--hf-radius) var(--hf-radius) 0 0;
}
.${scopeClass} .hf-panel-tabs {
border-bottom: 1px solid var(--hf-panel-border);
}
.${scopeClass} .hf-panel-body {
padding: 12px 13px;
}
.${scopeClass}.hf-panel--page .hf-panel-body {
max-height: min(70vh, 640px);
overflow-y: auto;
}
.${scopeClass}.hf-panel--page.hf-panel--grow .hf-panel-body {
max-height: none;
overflow: visible;
}
.${scopeClass}.hf-overlay,
.${scopeClass} .hf-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
}
.${scopeClass}.hf-modal,
.${scopeClass} .hf-modal {
background: #1f2127;
border: 1px solid var(--hf-panel-border);
border-radius: var(--hf-radius);
width: min(440px, 92vw);
max-height: 84vh;
overflow-y: auto;
padding: 18px;
color: var(--hf-text);
}
.${scopeClass}.hf-modal h2,
.${scopeClass} .hf-modal h2 {
margin: 0 0 14px;
font-size: 15px;
color: var(--hf-accent-text);
display: flex;
justify-content: space-between;
align-items: center;
}
.${scopeClass} .hf-mini {
font-size: 10px;
color: var(--hf-text-dim);
margin-top: 3px;
line-height: 1.4;
}
`;
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = css;
document.head.appendChild(style);
}
var theme = {
tokens,
injectStyles
};
// src/shared/storage.js
var PREFIX = "smart.torn";
var LEGACY_PREFIX = "hf.torn";
function namespaced(key) {
return `${PREFIX}.${key}`;
}
function legacyNamespaced(key) {
return `${LEGACY_PREFIX}.${key}`;
}
function getLocal(key, fallback = null) {
try {
const raw = localStorage.getItem(namespaced(key));
if (raw !== null) {
return JSON.parse(raw);
}
const legacyRaw = localStorage.getItem(legacyNamespaced(key));
if (legacyRaw !== null) {
localStorage.setItem(namespaced(key), legacyRaw);
return JSON.parse(legacyRaw);
}
return fallback;
} catch {
return fallback;
}
}
function setLocal(key, value) {
localStorage.setItem(namespaced(key), JSON.stringify(value));
}
function removeLocal(key) {
localStorage.removeItem(namespaced(key));
localStorage.removeItem(legacyNamespaced(key));
}
var DB_NAME = "smart-torn";
var LEGACY_DB_NAME = "heartflower-torn";
var DB_VERSION = 1;
var dbPromise = null;
function openDbByName(name) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, DB_VERSION);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains("races")) {
const store = db.createObjectStore("races", { keyPath: "id" });
store.createIndex("timestamp", "timestamp", { unique: false });
store.createIndex("track", "track", { unique: false });
store.createIndex("carKey", "carKey", { unique: false });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error ?? new Error("IndexedDB open failed"));
});
}
async function migrateLegacyDb(target) {
if (typeof indexedDB.databases !== "function") {
}
let legacy;
try {
legacy = await openDbByName(LEGACY_DB_NAME);
} catch {
return;
}
if (!legacy.objectStoreNames.contains("races") || !target.objectStoreNames.contains("races")) {
legacy.close();
return;
}
const rows = await new Promise((resolve, reject) => {
const tx = legacy.transaction("races", "readonly");
const request = tx.objectStore("races").getAll();
request.onsuccess = () => resolve(
/** @type {Record<string, unknown>[]} */
request.result ?? []
);
request.onerror = () => reject(request.error ?? new Error("Legacy IDB read failed"));
});
if (rows.length > 0) {
await new Promise((resolve, reject) => {
const tx = target.transaction("races", "readwrite");
const store = tx.objectStore("races");
for (const row of rows) {
store.put(row);
}
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error ?? new Error("Legacy IDB migrate failed"));
});
}
legacy.close();
}
function openDb() {
if (dbPromise) {
return dbPromise;
}
dbPromise = (async () => {
const db = await openDbByName(DB_NAME);
const migratedKey = "idb.migratedFromHeartflower";
if (getLocal(migratedKey, false) !== true) {
try {
await migrateLegacyDb(db);
} catch {
}
setLocal(migratedKey, true);
}
return db;
})();
return dbPromise;
}
async function withStore(storeName, mode, run) {
const db = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, mode);
const store = tx.objectStore(storeName);
const request = run(store);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
});
}
async function replaceAllRecords(storeName, records) {
const db = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, "readwrite");
const store = tx.objectStore(storeName);
store.clear();
for (const record of records) {
store.put(record);
}
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB replaceAll failed"));
});
}
async function putRecord(storeName, record) {
await withStore(storeName, "readwrite", (store) => store.put(record));
}
async function deleteRecord(storeName, key) {
await withStore(storeName, "readwrite", (store) => store.delete(key));
}
async function getRecord(storeName, key) {
return withStore(storeName, "readonly", (store) => store.get(key));
}
async function getAllRecords(storeName, query, limit) {
const db = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, "readonly");
const store = tx.objectStore(storeName);
const request = store.getAll(query, limit);
request.onsuccess = () => resolve(
/** @type {Record<string, unknown>[]} */
request.result ?? []
);
request.onerror = () => reject(request.error ?? new Error("IndexedDB getAll failed"));
});
}
async function getByIndex(storeName, indexName, key) {
return withStore(storeName, "readonly", (store) => store.index(indexName).getAll(key));
}
var storage = {
PREFIX,
LEGACY_PREFIX,
getLocal,
setLocal,
removeLocal,
putRecord,
deleteRecord,
getRecord,
getAllRecords,
getByIndex,
replaceAllRecords
};
// src/shared/dom.js
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitFor(selector, options = {}) {
const { timeout = 1e4, interval = 100, root = document } = options;
const start = Date.now();
while (Date.now() - start < timeout) {
const el = root.querySelector(selector);
if (el) {
return el;
}
await sleep(interval);
}
throw new Error(`waitFor timeout: ${selector}`);
}
async function waitForOptional(selector, options = {}) {
try {
return await waitFor(selector, options);
} catch {
return null;
}
}
function isHeaderBarNode(node) {
if (!(node instanceof HTMLElement)) {
return node.parentElement ? isHeaderBarNode(node.parentElement) : false;
}
return Boolean(node.closest(
'a[class*="bar-link"], [class*="bar-stats"], #header, header, [class*="topBar"], [class*="top-bar"], [class*="status-bar"]'
));
}
function isHeaderBarOnlyMutations(mutations) {
return mutations.length > 0 && mutations.every((mutation) => isHeaderBarNode(mutation.target));
}
function observeMutations(root, callback, options = {}) {
const { debounce = 50, ignoreHeaderBars = true } = options;
let timer = null;
const observer = new MutationObserver((mutations) => {
if (ignoreHeaderBars && isHeaderBarOnlyMutations(mutations)) {
return;
}
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(callback, debounce);
});
observer.observe(root, { childList: true, subtree: true });
callback();
return () => {
if (timer) {
clearTimeout(timer);
}
observer.disconnect();
};
}
function onAnchorClick(callback) {
const handler = (event) => {
const target = (
/** @type {Element} */
event.target
);
if (target.tagName === "A" || target.closest("a")) {
setTimeout(callback, 150);
}
};
document.body.addEventListener("click", handler);
return () => document.body.removeEventListener("click", handler);
}
function normalizeText(text) {
return text.replace(/\s+/g, " ").trim();
}
var dom = {
sleep,
waitFor,
waitForOptional,
observeMutations,
onAnchorClick,
normalizeText
};
// src/shared/mount.js
var MOUNT_PRESETS = {
racing: {
waitFor: ["#racingMainContainer"],
target: "#racingMainContainer",
position: "before"
},
gym: {
waitFor: ["#gymroot"],
target: "#gymroot",
position: "prepend"
},
gymBar: {
waitFor: ["#gymroot"],
target: "#gymroot",
position: "before"
},
stocksBar: {
waitFor: ["#stockmarketroot"],
target: "#stockmarketroot",
position: "before"
},
disposalBar: {
waitFor: ['[class*="disposal-root"]'],
target: '[class*="disposal-root"]',
position: "before"
},
contentTitle: {
waitFor: [".content-title", ".body > .content-title", "div.content-title"],
target: ".content-title",
position: "after"
}
};
function resolveMountConfig(mount2) {
if (!mount2) {
return MOUNT_PRESETS.contentTitle;
}
if (typeof mount2 === "string") {
if (mount2 === "page") {
return MOUNT_PRESETS.contentTitle;
}
return MOUNT_PRESETS[mount2] ?? MOUNT_PRESETS.contentTitle;
}
return mount2;
}
function findMountTarget(config) {
const pageReady = config.waitFor.some((selector) => document.querySelector(selector));
if (!pageReady) {
return null;
}
return document.querySelector(config.target);
}
function insertAt(target, element, position) {
switch (position) {
case "before":
target.insertAdjacentElement("beforebegin", element);
break;
case "after":
target.insertAdjacentElement("afterend", element);
break;
case "prepend":
target.prepend(element);
break;
case "append":
target.append(element);
break;
}
}
function isMountedAt(element, target, position) {
if (!element.isConnected) {
return false;
}
switch (position) {
case "before":
return element.nextElementSibling === target;
case "after":
return element.previousElementSibling === target;
case "prepend":
return target.firstElementChild === element;
case "append":
return target.lastElementChild === element;
default:
return false;
}
}
function mountElement(config, element) {
const target = findMountTarget(config);
if (!target) {
return false;
}
if (isMountedAt(element, target, config.position)) {
return true;
}
insertAt(target, element, config.position);
return true;
}
function getMountWatchRoot(config) {
const target = findMountTarget(config);
if (!target) {
return null;
}
if (config.position === "before" || config.position === "after") {
return target.parentElement;
}
return target;
}
function isOwnPanelMutation(mutations) {
return mutations.every((mutation) => {
if (mutation.target instanceof HTMLElement && mutation.target.closest(".hf-torn")) {
return true;
}
for (const node of mutation.addedNodes) {
if (node instanceof HTMLElement && (node.classList.contains("hf-torn") || node.closest(".hf-torn"))) {
continue;
}
if (node.nodeType === Node.TEXT_NODE && node.parentElement?.closest(".hf-torn")) {
continue;
}
return false;
}
for (const node of mutation.removedNodes) {
if (node instanceof HTMLElement && (node.classList.contains("hf-torn") || node.closest(".hf-torn"))) {
continue;
}
if (node.nodeType === Node.TEXT_NODE && node.parentElement?.closest(".hf-torn")) {
continue;
}
return false;
}
return true;
});
}
function watchMount(config, getElement) {
let stopped = false;
const start = Date.now();
const timeoutMs = 15e3;
let observer = null;
let debounceTimer = null;
let watchedRoot = null;
const disconnectObserver = () => {
observer?.disconnect();
observer = null;
watchedRoot = null;
};
const tryMount = () => {
if (stopped) {
return;
}
const element = getElement();
mountElement(config, element);
};
const scheduleTryMount = () => {
if (debounceTimer !== null) {
window.clearTimeout(debounceTimer);
}
debounceTimer = window.setTimeout(() => {
debounceTimer = null;
tryMount();
}, 80);
};
const attachObserver = () => {
const watchRoot = getMountWatchRoot(config);
if (!watchRoot || watchRoot === watchedRoot) {
return Boolean(watchRoot);
}
disconnectObserver();
watchedRoot = watchRoot;
observer = new MutationObserver((mutations) => {
if (isOwnPanelMutation(mutations)) {
return;
}
scheduleTryMount();
});
observer.observe(watchRoot, {
childList: true,
subtree: config.position === "prepend" || config.position === "append"
});
return true;
};
const poll = window.setInterval(() => {
tryMount();
attachObserver();
if (findMountTarget(config) || Date.now() - start > timeoutMs) {
window.clearInterval(poll);
}
}, 250);
tryMount();
attachObserver();
return () => {
stopped = true;
window.clearInterval(poll);
if (debounceTimer !== null) {
window.clearTimeout(debounceTimer);
}
disconnectObserver();
};
}
var mount = {
MOUNT_PRESETS,
resolveMountConfig,
findMountTarget,
insertAt,
isMountedAt,
mountElement,
watchMount
};
// src/shared/api.js
var API_BASE = "https://api.torn.com/v2";
var SETTINGS_KEY = "smart.torn.settings";
var LEGACY_SETTINGS_KEY = "hf.torn.settings";
function loadSettings() {
try {
let raw = GM_getValue(SETTINGS_KEY, null);
if (raw == null || raw === "" || raw === "{}") {
const legacy = GM_getValue(LEGACY_SETTINGS_KEY, null);
if (legacy != null && legacy !== "" && legacy !== "{}") {
GM_setValue(SETTINGS_KEY, typeof legacy === "string" ? legacy : JSON.stringify(legacy));
raw = legacy;
}
}
const parsed = typeof raw === "string" ? JSON.parse(raw || "{}") : raw ?? {};
return {
apiKey: String(parsed?.apiKey ?? "").trim()
};
} catch {
return { apiKey: "" };
}
}
function saveSettings(patch) {
const current = loadSettings();
const next = JSON.stringify({ ...current, ...patch });
GM_setValue(SETTINGS_KEY, next);
}
function parseTornError(json) {
if (!json || typeof json !== "object" || !("error" in json)) {
return null;
}
const err = (
/** @type {{ error?: unknown }} */
json.error
);
if (typeof err === "string") {
return new Error(err);
}
if (err && typeof err === "object") {
const detail = (
/** @type {{ error?: string, message?: string, code?: number }} */
err
);
return new Error(detail.error || detail.message || `Torn API error (${detail.code ?? "unknown"})`);
}
return new Error("Torn API error");
}
function tornGet(path, params, apiKey) {
const url = new URL(`${API_BASE}/${path.replace(/^\//, "")}`);
url.searchParams.set("key", apiKey);
for (const [key, value] of Object.entries(params ?? {})) {
if (value !== void 0 && value !== "") {
url.searchParams.set(key, String(value));
}
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url.toString(),
timeout: 2e4,
onload: (response) => {
try {
const json = JSON.parse(response.responseText || "{}");
const apiError = parseTornError(json);
if (apiError) {
reject(apiError);
return;
}
resolve(json);
} catch (error) {
reject(error instanceof Error ? error : new Error("Invalid API response"));
}
},
onerror: () => reject(new Error("Network error")),
ontimeout: () => reject(new Error("API request timed out"))
});
});
}
var api = {
loadSettings,
saveSettings,
parseTornError,
tornGet,
SETTINGS_KEY,
LEGACY_SETTINGS_KEY
};
// src/shared/ui.js
function panelCollapsedKey(panelId) {
return `panel.${panelId}.collapsed`;
}
function readPanelCollapsed(panelId, legacyKeys = []) {
const stored = getLocal(panelCollapsedKey(panelId), void 0);
if (stored === true || stored === "true") {
return true;
}
if (stored === false || stored === "false") {
return false;
}
for (const key of legacyKeys) {
const raw = localStorage.getItem(key);
if (raw === "true") {
setLocal(panelCollapsedKey(panelId), true);
return true;
}
if (raw === "false") {
setLocal(panelCollapsedKey(panelId), false);
return false;
}
}
return false;
}
var PANELS = /* @__PURE__ */ new Map();
function createPanel(options) {
const existing = PANELS.get(options.id);
if (existing?.element.isConnected) {
return existing;
}
if (existing) {
PANELS.delete(options.id);
}
injectStyles();
const mountOption = options.mount ?? "racing";
const isFixed = mountOption === "fixed";
const mountConfig = isFixed ? null : resolveMountConfig(mountOption);
const pageBody = options.pageBody ?? "cap";
const growPageBody = !isFixed && pageBody === "grow";
const root = document.createElement("section");
root.id = options.id;
root.className = isFixed ? "hf-torn hf-panel hf-panel--fixed" : `hf-torn hf-panel hf-panel--page${growPageBody ? " hf-panel--grow" : ""}`;
applyPanelLayout(root, isFixed, options.position ?? "bottom-right");
const header = document.createElement("div");
header.className = "hf-panel-header";
header.style.cssText = `
display: flex;
align-items: center;
gap: 10px;
padding: 9px 13px;
cursor: pointer;
user-select: none;
`;
const titleWrap = document.createElement("div");
titleWrap.style.display = "flex";
titleWrap.style.alignItems = "center";
titleWrap.style.gap = "10px";
titleWrap.style.flex = "1";
titleWrap.style.minWidth = "0";
if (options.badge) {
const badge = document.createElement("span");
badge.className = "hf-badge";
badge.textContent = options.badge;
titleWrap.appendChild(badge);
} else if (options.title) {
const title = document.createElement("div");
title.textContent = options.title;
title.style.fontWeight = "700";
title.style.fontSize = "13px";
title.style.color = "var(--hf-accent-text)";
titleWrap.appendChild(title);
}
const right = document.createElement("div");
right.className = "hf-panel-right";
right.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
flex-shrink: 0;
`;
const subtitle = document.createElement("div");
subtitle.className = "hf-muted hf-panel-subtitle";
subtitle.style.cssText = `
font-size: 11px;
max-width: min(280px, 40vw);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
`;
const gear = document.createElement("button");
gear.type = "button";
gear.className = "hf-panel-gear";
gear.title = "Settings";
gear.innerHTML = "⚙";
gear.style.cssText = `
font-size: 15px;
color: var(--hf-text-muted);
background: none;
border: none;
cursor: pointer;
padding: 0 4px;
line-height: 1;
flex-shrink: 0;
position: relative;
z-index: 2;
`;
const openSettings = (event) => {
event.preventDefault();
event.stopPropagation();
options.onSettings?.();
};
gear.addEventListener("mousedown", openSettings);
gear.addEventListener("click", openSettings);
const toggle = document.createElement("span");
toggle.className = "hf-panel-chev";
toggle.textContent = "\u25BC";
toggle.style.cssText = "font-size:12px;color:var(--hf-text-muted);transition:transform .2s;flex-shrink:0;";
right.appendChild(subtitle);
if (options.onSettings) {
right.appendChild(gear);
}
right.appendChild(toggle);
header.appendChild(titleWrap);
header.appendChild(right);
const tabBar = document.createElement("div");
tabBar.className = "hf-panel-tabs";
tabBar.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 6px 8px;
`;
const body = document.createElement("div");
body.className = "hf-panel-body";
if (growPageBody) {
body.style.maxHeight = "none";
body.style.overflow = "visible";
}
root.appendChild(header);
if (options.tabs.length > 1) {
root.appendChild(tabBar);
}
root.appendChild(body);
let collapsed = readPanelCollapsed(options.id, options.collapsedLegacyKeys);
let activeTabId = options.tabs[0]?.id ?? "";
const tabButtons = /* @__PURE__ */ new Map();
const setCollapsed = (value) => {
collapsed = value;
const hideBody = collapsed;
if (options.tabs.length > 1) {
tabBar.style.display = collapsed ? "none" : "flex";
}
body.style.display = hideBody ? "none" : "block";
toggle.style.transform = collapsed ? "rotate(-90deg)" : "";
root.classList.toggle("is-collapsed", collapsed);
setLocal(panelCollapsedKey(options.id), value);
};
let renderGeneration = 0;
const renderActiveTab = async () => {
const generation = ++renderGeneration;
const tab = options.tabs.find((item) => item.id === activeTabId);
body.replaceChildren();
if (!tab) {
body.textContent = "No tab selected.";
return;
}
const content = await tab.render();
if (generation !== renderGeneration) {
return;
}
body.replaceChildren();
body.appendChild(content);
};
const setTab = (id) => {
activeTabId = id;
tabButtons.forEach((btn, tabId) => {
btn.classList.toggle("is-active", tabId === id);
});
renderActiveTab();
};
if (options.tabs.length > 1) {
for (const tab of options.tabs) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "hf-btn";
btn.textContent = tab.label;
btn.addEventListener("click", (event) => {
event.stopPropagation();
setTab(tab.id);
});
tabButtons.set(tab.id, btn);
tabBar.appendChild(btn);
}
}
header.addEventListener("click", (event) => {
if (
/** @type {Element} */
event.target.closest(".hf-panel-gear")
) {
return;
}
if (
/** @type {Element} */
event.target.closest("button")
) {
return;
}
setCollapsed(!collapsed);
});
gear.addEventListener("mouseenter", () => {
gear.style.color = "#fff";
});
gear.addEventListener("mouseleave", () => {
gear.style.color = "var(--hf-text-muted)";
});
setTab(activeTabId);
setCollapsed(collapsed);
let stopWatching = null;
const ensureMounted = () => {
if (isFixed) {
if (root.parentElement !== document.body) {
document.body.appendChild(root);
}
return;
}
if (mountConfig) {
mountElement(mountConfig, root);
}
};
if (isFixed) {
ensureMounted();
} else if (mountConfig) {
stopWatching = watchMount(mountConfig, () => root);
}
const panel2 = {
element: root,
setTab,
getActiveTabId: () => activeTabId,
refresh: () => {
if (options.getSubtitle) {
subtitle.textContent = options.getSubtitle() ?? "";
}
renderActiveTab();
},
setSubtitle: (text) => {
subtitle.textContent = text;
},
ensureMounted,
destroy: () => {
stopWatching?.();
root.remove();
PANELS.delete(options.id);
}
};
if (options.getSubtitle) {
subtitle.textContent = options.getSubtitle() ?? "";
}
PANELS.set(options.id, panel2);
return panel2;
}
function applyPanelLayout(el, isFixed, position) {
if (!isFixed) {
el.style.cssText = `
position: static;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
`;
return;
}
el.style.cssText = `
position: fixed;
z-index: 9999;
width: min(420px, calc(100vw - 24px));
max-height: min(70vh, 560px);
display: flex;
flex-direction: column;
overflow: hidden;
`;
applyFixedPosition(el, position);
}
function applyFixedPosition(el, position) {
el.style.top = "";
el.style.right = "";
el.style.bottom = "";
el.style.left = "";
switch (position) {
case "top-right":
el.style.top = "12px";
el.style.right = "12px";
break;
case "bottom-left":
el.style.bottom = "12px";
el.style.left = "12px";
break;
case "bottom-right":
default:
el.style.bottom = "12px";
el.style.right = "12px";
break;
}
}
function emptyState(message) {
const div = document.createElement("div");
div.className = "hf-muted";
div.textContent = message;
return div;
}
var ui = {
createPanel,
emptyState
};
// src/shared/percent.js
function parsePercent(raw, fallback = 0) {
if (raw === null || raw === void 0 || raw === "") {
return fallback;
}
if (typeof raw === "number") {
return Number.isFinite(raw) ? raw : fallback;
}
const cleaned = String(raw).trim().replace("%", "").replace(",", ".");
const value = parseFloat(cleaned);
return Number.isFinite(value) ? value : fallback;
}
function weightToPercent(weight) {
return Math.round(weight * 1e3) / 10;
}
function weightSharePercent(weight, weights) {
const sum = Object.values(weights).reduce((total, w) => total + (w > 0 ? w : 0), 0);
if (sum <= 0 || weight <= 0) {
return 0;
}
return weight / sum * 100;
}
function percentToWeight(raw, fallback = 0) {
const pct = parsePercent(raw, fallback * 100);
return pct / 100;
}
function weightsToShares(weights) {
const sum = Object.values(weights).reduce((total, w) => total + (w > 0 ? w : 0), 0);
if (sum <= 0) {
return {};
}
const out = {};
for (const [stat, w] of Object.entries(weights)) {
out[stat] = w > 0 ? w / sum * 100 : 0;
}
return out;
}
function normalizeWeightsToShares(weights) {
const sum = Object.values(weights).reduce((total, w) => total + (w > 0 ? w : 0), 0);
if (sum <= 0) {
return weights;
}
const out = {};
for (const [stat, w] of Object.entries(weights)) {
out[stat] = w > 0 ? w / sum * 100 : 0;
}
return out;
}
function parseShareInputs(inputsByStat) {
const raw = {};
let sum = 0;
for (const [stat, value] of Object.entries(inputsByStat)) {
const pct = Math.max(0, parsePercent(value, 0));
raw[stat] = pct;
sum += pct;
}
if (sum <= 0) {
return raw;
}
const out = {};
for (const [stat, pct] of Object.entries(raw)) {
out[stat] = pct > 0 ? pct / sum * 100 : 0;
}
return out;
}
function bonusesToMultiplier(bonuses) {
const f = parsePercent(bonuses?.faction, 0);
const e = parsePercent(bonuses?.education, 0);
const p = parsePercent(bonuses?.property, 0);
return (1 + f / 100) * (1 + e / 100) * (1 + p / 100);
}
function multiplierToTotalPercent(multiplier) {
if (!Number.isFinite(multiplier) || multiplier <= 1) {
return 0;
}
return Math.round((multiplier - 1) * 1e3) / 10;
}
function formatPercent(pct, digits = 1) {
const rounded = Math.round(pct * Math.pow(10, digits)) / Math.pow(10, digits);
const text = Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(digits);
return `${text}%`;
}
function formatWeightSummary(weights, shortLabels) {
const parts = Object.entries(weights).filter(([, w]) => w > 0).sort((a, b) => b[1] - a[1]).map(([stat, w]) => `${shortLabels[stat] ?? stat} ${formatPercent(weightSharePercent(w, weights), 0)}`);
return parts.join(" \xB7 ");
}
var percent = {
parsePercent,
weightToPercent,
weightSharePercent,
percentToWeight,
weightsToShares,
normalizeWeightsToShares,
parseShareInputs,
bonusesToMultiplier,
multiplierToTotalPercent,
formatPercent,
formatWeightSummary
};
// src/shared/bars.js
var FILL_SELECTORS = [
'[class*="bar-fill"]',
'[class*="barFill"]',
'[class*="progress-fill"]',
'[class*="progressFill"]',
'[class*="progressBar"]',
'[class*="progress-bar"]'
].join(", ");
function applyBarFill(box, pct) {
const clamped = Math.max(0, Math.min(100, pct));
let updated = false;
box.querySelectorAll(FILL_SELECTORS).forEach((fill) => {
if (!(fill instanceof HTMLElement)) {
return;
}
fill.style.width = `${clamped}%`;
fill.style.transform = "";
updated = true;
});
const progressbar = box.matches('[role="progressbar"]') ? box : box.querySelector('[role="progressbar"]');
if (progressbar instanceof HTMLElement) {
progressbar.setAttribute("aria-valuenow", String(Math.round(clamped)));
const inner = progressbar.firstElementChild;
if (inner instanceof HTMLElement) {
inner.style.width = `${clamped}%`;
inner.style.transform = `scaleX(${clamped / 100})`;
inner.style.transformOrigin = "left center";
updated = true;
}
}
if (!updated) {
for (const child of box.children) {
if (!(child instanceof HTMLElement) || child.tagName === "P") {
continue;
}
if (child.querySelector(FILL_SELECTORS)) {
continue;
}
child.style.width = `${clamped}%`;
child.style.transform = `scaleX(${clamped / 100})`;
child.style.transformOrigin = "left center";
updated = true;
}
}
}
function applyBarVisual(box, bar) {
if (!box || !bar?.max) {
return;
}
const val = box.querySelector('p[class*="bar-value"], [class*="bar-value"]');
if (val) {
val.textContent = `${bar.current}/${bar.max}`;
}
const pct = bar.max > 0 ? bar.current / bar.max * 100 : 0;
applyBarFill(box, pct);
box.querySelectorAll("[aria-valuemax]").forEach((el) => {
el.setAttribute("aria-valuenow", String(bar.current));
el.setAttribute("aria-valuemax", String(bar.max));
});
}
function findBarBox(label) {
let labels = document.querySelectorAll('p[class*="bar-name"]');
if (!labels.length) {
labels = document.querySelectorAll(".wai");
}
for (const el of labels) {
if (el.textContent.trim().replace(":", "") === label) {
return el.closest('[class*="bar-stats"]') || el.parentElement;
}
}
const slug = label.toLowerCase();
const link = document.querySelector(
`a.bar-link[class*="${slug}"], a[class*="bar-link"][class*="${slug}"]`
);
return link;
}
function syncBarVisuals(bars2) {
if (bars2.energy) {
applyBarVisual(findBarBox("Energy"), bars2.energy);
}
if (bars2.happy) {
applyBarVisual(findBarBox("Happy"), bars2.happy);
}
if (bars2.nerve) {
applyBarVisual(findBarBox("Nerve"), bars2.nerve);
}
if (bars2.life) {
applyBarVisual(findBarBox("Life"), bars2.life);
}
}
var bars = {
applyBarVisual,
findBarBox,
syncBarVisuals
};
// src/shared/index.js
var HF = {
version: "1.0.0",
theme,
storage,
dom,
mount,
api,
ui,
percent,
bars
};
// src/stocks/legacy-app.js
function bootstrapStocksApp(opts = {}) {
var $;
const MOUNT_SELECTOR = opts.mountRoot || "#smart-stocks-root";
function resolveJQuery() {
if (typeof jQuery !== "undefined" && jQuery.fn) {
return jQuery;
}
if (typeof unsafeWindow !== "undefined" && unsafeWindow.jQuery && unsafeWindow.jQuery.fn) {
return unsafeWindow.jQuery;
}
if (typeof window !== "undefined" && window.jQuery && window.jQuery.fn) {
return window.jQuery;
}
return null;
}
function mountVaultHtml(html) {
const cleaned = html.replace(/<div class="alfa-vault-header">[\s\S]*?<\/div>\s*<\/div>\s*/g, "");
$(MOUNT_SELECTOR).html(cleaned);
}
const STOCK_DATA = {
"ASS": { base: 1e6, type: "A" },
"BAG": { base: 3e6, type: "A" },
"CNC": { base: 75e5, type: "A" },
"EWM": { base: 1e6, type: "A" },
"ELT": { base: 5e6, type: "P" },
"EVL": { base: 1e5, type: "A" },
"FHG": { base: 2e6, type: "A" },
"GRN": { base: 5e5, type: "A" },
"CBD": { base: 35e4, type: "A" },
"HRG": { base: 1e7, type: "A" },
"IIL": { base: 1e6, type: "P" },
"IOU": { base: 3e6, type: "A" },
"IST": { base: 1e5, type: "P" },
"LAG": { base: 75e4, type: "A" },
"LOS": { base: 75e5, type: "P" },
"LSC": { base: 5e5, type: "A" },
"MCS": { base: 35e4, type: "A" },
"MSG": { base: 3e5, type: "P" },
"MUN": { base: 5e6, type: "A" },
"PRN": { base: 1e6, type: "A" },
"PTS": { base: 1e7, type: "A" },
"SYM": { base: 5e5, type: "A" },
"SYS": { base: 3e6, type: "P" },
"TCP": { base: 1e6, type: "P" },
"TMI": { base: 6e6, type: "A" },
"TGP": { base: 25e5, type: "P" },
"TCT": { base: 1e5, type: "A" },
"TSB": { base: 3e6, type: "A" },
"TCC": { base: 75e5, type: "A" },
"THS": { base: 15e4, type: "A" },
"TCI": { base: 15e5, type: "P" },
"TCM": { base: 1e6, type: "P" },
"WSU": { base: 1e6, type: "P" },
"WLT": { base: 9e6, type: "P" },
"YAZ": { base: 1e6, type: "P" }
};
let STOCK_ID_MAP = {
1: "TCI",
2: "TCC",
3: "SYS",
4: "LAG",
5: "IOU",
6: "GRN",
7: "THS",
8: "CBD",
9: "TCT",
10: "EVL",
11: "MCS",
12: "WSU",
13: "IIL",
14: "FHG",
15: "SYM",
16: "LSC",
17: "PRN",
18: "EWM",
19: "TCM",
20: "MSG",
21: "MUN",
22: "YAZ",
23: "IST",
24: "BAG",
25: "ASS",
26: "CNC",
27: "TMI",
28: "ELT",
29: "HRG",
30: "TGP",
31: "TSB",
32: "WLT",
33: "LOS",
34: "TCP",
35: "PTS"
};
let SYMBOL_TO_ID = {};
for (const [id, sym] of Object.entries(STOCK_ID_MAP)) {
SYMBOL_TO_ID[sym] = parseInt(id);
}
function buildStockIdMap() {
for (const [sym, domId] of Object.entries(stockId)) {
const numId = parseInt(domId.replace("stock_", ""));
if (!isNaN(numId)) {
STOCK_ID_MAP[numId] = sym;
SYMBOL_TO_ID[sym] = numId;
}
}
}
const LOG_TYPE_BUY = 5510;
const LOG_TYPE_SELL = 5511;
let portfolioData = {
transactions: [],
// All stock transactions from API log
lastSyncTimestamp: 0,
// Last transaction timestamp we synced
costBasis: {},
// Per-stock cost basis tracking { symbol: { totalShares: 0, totalCost: 0, lots: [] } }
realizedPL: 0,
// Total realized profit/loss
lastFullSync: 0
// Timestamp of last full sync
};
try {
let savedPortfolio = JSON.parse(localStorage.getItem("alfa_portfolio_data"));
if (savedPortfolio) {
portfolioData = { ...portfolioData, ...savedPortfolio };
}
} catch (e) {
console.error("Failed to load portfolio data:", e);
}
let swingTradeData = {
activePositions: [],
// Current open swing trades
completedTrades: [],
// Historical completed trades
watchlist: [],
// Stocks being watched for entry
settings: {
defaultTargetPercent: 3,
// Default take profit %
defaultStopPercent: 2,
// Default stop loss %
minSwingPercent: 0.5,
// Minimum price swing to trigger signal
rsiOversold: 30,
// RSI threshold for buy signals
rsiOverbought: 70,
// RSI threshold for sell signals
bollingerEntryThreshold: 0.2,
// Entry when below this position (0-1)
bollingerExitThreshold: 0.8,
// Exit when above this position (0-1)
adxMinThreshold: 25,
// ADX must be >= this for full RSI weight (filters weak trends)
adxPartialThreshold: 20,
// Between 20-25, RSI gets 50% weight; below 20, RSI ignored
volumeMinMultiplier: 1,
// Volume must be >= 7d avg for full signal (filters fakeouts)
volumePartialMultiplier: 0.8,
// Between 0.8-1, partial weight; below 0.8, discount
atrTargetMultiplier: 2,
// Target = ATR% * this (e.g. 2x ATR for take profit)
atrStopMultiplier: 1.5,
// Stop = ATR% * this (e.g. 1.5x ATR for stop loss)
enableAlerts: true,
// Show visual alerts for signals
autoTrack: false
// Auto-add buy transactions as positions
},
lastSignalCheck: 0
};
try {
let savedSwingData = JSON.parse(localStorage.getItem("alfa_swing_trades"));
if (savedSwingData) {
swingTradeData = { ...swingTradeData, ...savedSwingData };
swingTradeData.completedTrades.forEach((t) => {
if (!t.tags) t.tags = [];
});
}
} catch (e) {
console.error("Failed to load swing trade data:", e);
}
let lastClosedPosition = null;
let undoCloseTimeout = null;
function saveSwingTradeData() {
localStorage.setItem("alfa_swing_trades", JSON.stringify(swingTradeData));
}
function generateTradeId() {
return `ST_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
}
function addSwingPosition(symbol, entryPrice, shares, targetPercent = null, stopPercent = null, notes = "", targetPrice = null, stopPrice = null) {
const settings = swingTradeData.settings;
targetPercent = targetPercent ?? settings.defaultTargetPercent;
stopPercent = stopPercent ?? settings.defaultStopPercent;
let finalTargetPrice, finalStopPrice, finalTargetPercent, finalStopPercent;
if (targetPrice != null && targetPrice > 0) {
finalTargetPrice = targetPrice;
finalTargetPercent = entryPrice > 0 ? (targetPrice / entryPrice - 1) * 100 : 0;
} else {
finalTargetPrice = entryPrice * (1 + targetPercent / 100);
finalTargetPercent = targetPercent;
}
if (stopPrice != null && stopPrice > 0) {
finalStopPrice = stopPrice;
finalStopPercent = entryPrice > 0 ? (1 - stopPrice / entryPrice) * 100 : 0;
} else {
finalStopPrice = entryPrice * (1 - stopPercent / 100);
finalStopPercent = stopPercent;
}
const position = {
id: generateTradeId(),
symbol: symbol.toUpperCase(),
type: "long",
entryPrice,
entryDate: Date.now(),
shares,
targetPrice: finalTargetPrice,
stopPrice: finalStopPrice,
targetPercent: finalTargetPercent,
stopPercent: finalStopPercent,
status: "active",
notes,
currentPrice: entryPrice,
currentPL: 0,
currentPLPercent: 0
};
swingTradeData.activePositions.push(position);
saveSwingTradeData();
return position;
}
function updatePositionPrice(positionId, currentPrice, opts2 = {}) {
const persist = opts2.persist !== false;
const position = swingTradeData.activePositions.find((p) => p.id === positionId);
if (!position) return null;
position.currentPrice = currentPrice;
position.currentPL = (currentPrice - position.entryPrice) * position.shares;
position.currentPLPercent = (currentPrice - position.entryPrice) / position.entryPrice * 100;
let statusChanged = false;
if (currentPrice >= position.targetPrice && position.status === "active") {
position.status = "target_hit";
statusChanged = true;
} else if (currentPrice <= position.stopPrice && position.status === "active") {
position.status = "stopped";
statusChanged = true;
}
if (persist && statusChanged) saveSwingTradeData();
return position;
}
function updateAllPositionPrices() {
let statusChanged = false;
for (const position of swingTradeData.activePositions) {
const currentPrice = getPrice(position.symbol);
if (currentPrice > 0) {
const before = position.status;
updatePositionPrice(position.id, currentPrice, { persist: false });
if (position.status !== before) statusChanged = true;
}
}
if (statusChanged) saveSwingTradeData();
}
function closeSwingPosition(positionId, exitPrice = null, exitReason = "manual_close", sharesToClose = null) {
const posIndex = swingTradeData.activePositions.findIndex((p) => p.id === positionId);
if (posIndex === -1) return null;
const position = swingTradeData.activePositions[posIndex];
exitPrice = exitPrice || position.currentPrice || getPrice(position.symbol);
const isFullClose = sharesToClose == null || sharesToClose >= position.shares;
const closeShares = isFullClose ? position.shares : Math.min(Math.max(1, Math.floor(sharesToClose)), position.shares - 1);
const grossProceeds = exitPrice * closeShares;
const fee = grossProceeds * 1e-3;
const netProceeds = grossProceeds - fee;
const cost = position.entryPrice * closeShares;
const completedTrade = {
id: isFullClose ? position.id : generateTradeId(),
symbol: position.symbol,
type: position.type,
entryPrice: position.entryPrice,
entryDate: position.entryDate,
exitPrice,
exitDate: Date.now(),
shares: closeShares,
grossPnl: grossProceeds - cost,
fee,
pnl: netProceeds - cost,
pnlPercent: cost > 0 ? (netProceeds - cost) / cost * 100 : 0,
holdingDays: Math.ceil((Date.now() - position.entryDate) / (1e3 * 60 * 60 * 24)),
exitReason,
notes: position.notes || "",
tags: position.tags || []
};
if (isFullClose) {
lastClosedPosition = { position: JSON.parse(JSON.stringify(position)), completedTrade };
if (undoCloseTimeout) clearTimeout(undoCloseTimeout);
undoCloseTimeout = setTimeout(() => {
lastClosedPosition = null;
}, 15e3);
swingTradeData.activePositions.splice(posIndex, 1);
} else {
position.shares -= closeShares;
}
swingTradeData.completedTrades.unshift(completedTrade);
if (swingTradeData.completedTrades.length > 100) {
swingTradeData.completedTrades = swingTradeData.completedTrades.slice(0, 100);
}
saveSwingTradeData();
return completedTrade;
}
function recalculateCompletedTrade(trade) {
const cost = trade.entryPrice * trade.shares;
const grossProceeds = trade.exitPrice * trade.shares;
const fee = grossProceeds * 1e-3;
const netProceeds = grossProceeds - fee;
trade.grossPnl = grossProceeds - cost;
trade.fee = fee;
trade.pnl = netProceeds - cost;
trade.pnlPercent = cost > 0 ? (netProceeds - cost) / cost * 100 : 0;
trade.holdingDays = Math.ceil((trade.exitDate - trade.entryDate) / (1e3 * 60 * 60 * 24));
return trade;
}
function undoClosePosition() {
if (!lastClosedPosition) return false;
const { position, completedTrade } = lastClosedPosition;
const idx = swingTradeData.completedTrades.findIndex((t) => t.id === completedTrade.id);
if (idx === -1) return false;
swingTradeData.completedTrades.splice(idx, 1);
swingTradeData.activePositions.unshift(position);
lastClosedPosition = null;
if (undoCloseTimeout) {
clearTimeout(undoCloseTimeout);
undoCloseTimeout = null;
}
saveSwingTradeData();
return true;
}
function showUndoCloseBanner() {
const existing = document.getElementById("swing-undo-banner");
if (existing) existing.remove();
const banner = document.createElement("div");
banner.id = "swing-undo-banner";
banner.className = "swing-undo-banner";
banner.innerHTML = `Position closed. <button id="swing-undo-btn" class="swing-undo-btn">Undo</button>`;
const swingEl = document.querySelector(".swing-content") || document.querySelector("#swing-content");
const parent = swingEl || document.body;
if (parent) {
parent.insertBefore(banner, parent.firstChild);
banner.querySelector("#swing-undo-btn").onclick = () => {
if (undoClosePosition()) {
$("#swing-positions-list").html(renderSwingPositions());
refreshSwingJournal();
bindSwingPositionActions();
}
banner.remove();
};
}
}
function deleteJournalEntry(tradeId) {
const idx = swingTradeData.completedTrades.findIndex((t) => t.id === tradeId);
if (idx === -1) return false;
swingTradeData.completedTrades.splice(idx, 1);
saveSwingTradeData();
return true;
}
function getSwingTradeStats(period = "all") {
const startTimestamp = getPeriodStartTimestamp(period);
const trades = swingTradeData.completedTrades.filter((t) => t.exitDate >= startTimestamp);
if (trades.length === 0) {
return { totalTrades: 0, winRate: 0, avgPnl: 0, avgPnlPercent: 0, totalPnl: 0, avgHoldDays: 0, bestTrade: null, worstTrade: null };
}
const winners = trades.filter((t) => t.pnl > 0);
const totalPnl = trades.reduce((sum, t) => sum + t.pnl, 0);
const totalPnlPercent = trades.reduce((sum, t) => sum + t.pnlPercent, 0);
const avgHoldDays = trades.reduce((sum, t) => sum + t.holdingDays, 0) / trades.length;
const sortedByPnl = [...trades].sort((a, b) => b.pnl - a.pnl);
return {
totalTrades: trades.length,
winners: winners.length,
losers: trades.length - winners.length,
winRate: winners.length / trades.length * 100,
avgPnl: totalPnl / trades.length,
avgPnlPercent: totalPnlPercent / trades.length,
totalPnl,
avgHoldDays,
bestTrade: sortedByPnl[0],
worstTrade: sortedByPnl[sortedByPnl.length - 1]
};
}
function analyzeSwingOpportunity(analysis) {
if (!analysis) return null;
const settings = swingTradeData.settings;
const signals = [];
let score = 0;
const adx = analysis.adx ?? 25;
const adxMin = settings.adxMinThreshold ?? 25;
const adxPartial = settings.adxPartialThreshold ?? 20;
const rsiAdxFactor = adx >= adxMin ? 1 : adx >= adxPartial ? 0.5 : 0;
let rsiWeight = 0;
if (analysis.rsi < settings.rsiOversold) {
rsiWeight = 30;
signals.push({ type: "rsi", direction: "bullish", message: `RSI oversold (${analysis.rsi.toFixed(0)})${rsiAdxFactor < 1 ? " [weak trend]" : ""}`, weight: 30 });
} else if (analysis.rsi < 40) {
rsiWeight = 15;
signals.push({ type: "rsi", direction: "bullish", message: `RSI low (${analysis.rsi.toFixed(0)})${rsiAdxFactor < 1 ? " [weak trend]" : ""}`, weight: 15 });
} else if (analysis.rsi > settings.rsiOverbought) {
rsiWeight = -30;
signals.push({ type: "rsi", direction: "bearish", message: `RSI overbought (${analysis.rsi.toFixed(0)})${rsiAdxFactor < 1 ? " [weak trend]" : ""}`, weight: -30 });
} else if (analysis.rsi > 60) {
rsiWeight = -15;
signals.push({ type: "rsi", direction: "bearish", message: `RSI elevated (${analysis.rsi.toFixed(0)})${rsiAdxFactor < 1 ? " [weak trend]" : ""}`, weight: -15 });
}
score += rsiWeight * rsiAdxFactor;
if (analysis.bollingerPosition <= settings.bollingerEntryThreshold) {
signals.push({ type: "bollinger", direction: "bullish", message: `Near lower Bollinger band`, weight: 25 });
score += 25;
} else if (analysis.bollingerPosition >= settings.bollingerExitThreshold) {
signals.push({ type: "bollinger", direction: "bearish", message: `Near upper Bollinger band`, weight: -25 });
score -= 25;
}
if (analysis.support > 0 && analysis.price <= analysis.support * 1.02) {
signals.push({ type: "support", direction: "bullish", message: `At support level ($${analysis.support.toFixed(2)})`, weight: 20 });
score += 20;
}
if (analysis.resistance > 0 && analysis.price >= analysis.resistance * 0.98) {
signals.push({ type: "resistance", direction: "bearish", message: `At resistance ($${analysis.resistance.toFixed(2)})`, weight: -20 });
score -= 20;
}
if (analysis.momentum7d < -3) {
signals.push({ type: "momentum", direction: "bullish", message: `Momentum reversal potential (${analysis.momentum7d.toFixed(1)}%)`, weight: 15 });
score += 15;
} else if (analysis.momentum7d > 5) {
signals.push({ type: "momentum", direction: "bearish", message: `Extended momentum (${analysis.momentum7d.toFixed(1)}%)`, weight: -15 });
score -= 15;
}
if (analysis.dipFrom7d < -3) {
signals.push({ type: "sma", direction: "bullish", message: `${analysis.dipFrom7d.toFixed(1)}% below 7-day avg`, weight: 20 });
score += 20;
} else if (analysis.dipFrom7d > 3) {
signals.push({ type: "sma", direction: "bearish", message: `+${analysis.dipFrom7d.toFixed(1)}% above 7-day avg`, weight: -20 });
score -= 20;
}
const volMult = analysis.volumeMultiplier ?? 1;
const volMin = settings.volumeMinMultiplier ?? 1;
const volPartial = settings.volumePartialMultiplier ?? 0.8;
const volumeFactor = volMult >= volMin ? 1 : volMult >= volPartial ? 0.5 : 0;
score *= volumeFactor;
let overallSignal = "neutral";
if (score >= 50) overallSignal = "strong_buy";
else if (score >= 25) overallSignal = "buy";
else if (score >= 10) overallSignal = "watch";
else if (score <= -50) overallSignal = "strong_sell";
else if (score <= -25) overallSignal = "sell";
else if (score <= -10) overallSignal = "take_profit";
const atr = analysis.atr ?? 0;
const atrPercent = analysis.price > 0 && atr > 0 ? atr / analysis.price * 100 : 0;
const atrTargetMult = settings.atrTargetMultiplier ?? 2;
const atrStopMult = settings.atrStopMultiplier ?? 1.5;
const suggestedTarget = Math.max(
settings.defaultTargetPercent,
atrPercent * atrTargetMult,
// ATR-based: 2x ATR % for target
analysis.volatility * 1.5,
// 1.5x volatility
analysis.resistance > 0 ? (analysis.resistance - analysis.price) / analysis.price * 100 : 0
);
const suggestedStop = Math.max(
settings.defaultStopPercent,
atrPercent * atrStopMult,
// ATR-based: 1.5x ATR % for stop
analysis.volatility * 1.2,
// 1.2x volatility for stop
analysis.support > 0 ? (analysis.price - analysis.support) / analysis.price * 100 + 0.5 : 0
);
return {
symbol: analysis.symbol,
price: analysis.price,
score,
signal: overallSignal,
signals,
suggestedTarget: Math.round(Math.min(suggestedTarget, 10) * 2) / 2,
// Cap at 10%, round to 0.5%
suggestedStop: Math.round(Math.min(suggestedStop, 5) * 2) / 2,
// Cap at 5%, round to 0.5%
volatility: analysis.volatility,
rsi: analysis.rsi,
adx,
atr: analysis.atr,
volumeMultiplier: analysis.volumeMultiplier,
bollingerPosition: analysis.bollingerPosition,
support: analysis.support,
resistance: analysis.resistance
};
}
async function scanSwingOpportunities() {
const opportunities = [];
const symbols = (vaultConfig.stocks || []).filter((sym) => STOCK_DATA[sym]);
if (symbols.length === 0) {
swingTradeData.lastSignalCheck = Date.now();
swingTradeData.lastSignalBySymbol = {};
return opportunities;
}
for (let i = 0; i < symbols.length; i++) {
const sym = symbols[i];
let analysis = vaultAnalysisCache[sym];
let fetched = false;
if (!analysis) {
analysis = await analyzeStockForVault(sym);
fetched = true;
}
if (analysis) {
const opportunity = analyzeSwingOpportunity(analysis);
if (opportunity) {
opportunity.hasActivePosition = swingTradeData.activePositions.some((p) => p.symbol === sym);
opportunity.currentShares = getOwnedShares(sym);
opportunities.push(opportunity);
}
}
if (fetched && i < symbols.length - 1) {
await new Promise((r) => setTimeout(r, 100));
}
}
opportunities.sort((a, b) => b.score - a.score);
swingTradeData.lastSignalCheck = Date.now();
swingTradeData.lastSignalBySymbol = {};
opportunities.forEach((o) => {
swingTradeData.lastSignalBySymbol[o.symbol] = o;
});
return opportunities;
}
async function getTopSwingSignals(limit = 5) {
const opportunities = await scanSwingOpportunities();
const buySignals = opportunities.filter((o) => ["strong_buy", "buy", "watch"].includes(o.signal)).slice(0, limit);
const sellSignals = opportunities.filter((o) => ["strong_sell", "sell", "take_profit"].includes(o.signal)).slice(0, limit);
return { buySignals, sellSignals, all: opportunities };
}
async function refreshPositionSignals() {
const positions = swingTradeData.activePositions;
if (positions.length === 0) return;
if (!swingTradeData.lastSignalBySymbol) swingTradeData.lastSignalBySymbol = {};
const symbols = [...new Set(positions.map((p) => p.symbol))];
for (const sym of symbols) {
let analysis = vaultAnalysisCache[sym];
if (!analysis) analysis = await analyzeStockForVault(sym);
if (analysis) {
const opportunity = analyzeSwingOpportunity(analysis);
if (opportunity) swingTradeData.lastSignalBySymbol[sym] = opportunity;
}
}
swingTradeData.lastSignalCheck = Date.now();
}
function formatSignalAge(msAgo) {
if (msAgo < 6e4) return "<1m";
if (msAgo < 36e5) return Math.floor(msAgo / 6e4) + "m";
if (msAgo < 864e5) return Math.floor(msAgo / 36e5) + "h";
return Math.floor(msAgo / 864e5) + "d";
}
const SIGNAL_STALE_MS = 30 * 60 * 1e3;
function renderSwingPositions() {
updateAllPositionPrices();
const positions = swingTradeData.activePositions;
if (positions.length === 0) {
return `<div class="swing-empty">
<div style="font-size:24px; margin-bottom:8px;">\u{1F4CA}</div>
<div>No active swing trades</div>
<div style="font-size:10px; color:#666; margin-top:5px;">Add a position or use signals to find opportunities</div>
</div>`;
}
let totalPL = 0;
let totalValue = 0;
const positionRows = positions.map((pos) => {
const currentPrice = getPrice(pos.symbol) || pos.currentPrice;
const pl = (currentPrice - pos.entryPrice) * pos.shares;
const plPercent = (currentPrice - pos.entryPrice) / pos.entryPrice * 100;
const value = currentPrice * pos.shares;
totalPL += pl;
totalValue += value;
const priceRange = pos.targetPrice - pos.stopPrice;
const currentProgress = currentPrice - pos.stopPrice;
const progressPercent = Math.max(0, Math.min(100, currentProgress / priceRange * 100));
const statusClass = pos.status === "target_hit" ? "target-hit" : pos.status === "stopped" ? "stopped" : "";
const statusIcon = pos.status === "target_hit" ? "\u{1F3AF}" : pos.status === "stopped" ? "\u{1F6D1}" : "";
const entryDate = new Date(pos.entryDate).toLocaleDateString();
const holdDays = Math.ceil((Date.now() - pos.entryDate) / (1e3 * 60 * 60 * 24));
const signalColors = { strong_buy: "#00e676", buy: "#66bb6a", watch: "#ffd54f", neutral: "#888", take_profit: "#ffb74d", sell: "#ff7043", strong_sell: "#ef5350" };
const signalLabels = { strong_buy: "STRONG BUY", buy: "BUY", watch: "WATCH", neutral: "NEUTRAL", take_profit: "TAKE PROFIT", sell: "SELL", strong_sell: "STRONG SELL" };
const opportunity = swingTradeData.lastSignalBySymbol?.[pos.symbol];
const msAgo = swingTradeData.lastSignalCheck ? Date.now() - swingTradeData.lastSignalCheck : Infinity;
const isStale = msAgo > SIGNAL_STALE_MS;
let signalBadge = "";
if (opportunity && opportunity.signal !== "neutral") {
const ageStr = formatSignalAge(msAgo);
const staleClass = isStale ? " swing-signal-stale" : "";
signalBadge = `<span class="swing-pos-signal${staleClass}" style="background:${signalColors[opportunity.signal]}22; color:${signalColors[opportunity.signal]};" title="${isStale ? "Stale - click Refresh signals" : ""}">${signalLabels[opportunity.signal]} (${ageStr})</span>`;
}
return `
<div class="swing-position ${statusClass}" data-id="${pos.id}">
<div class="swing-pos-header">
<div class="swing-pos-title-group">
<span class="swing-pos-symbol">${pos.symbol}</span>
${signalBadge}
</div>
<div class="swing-pos-header-right">
<span class="swing-pos-pl ${pl >= 0 ? "positive" : "negative"}">${pl >= 0 ? "+" : ""}${formatMoney(pl)} (${plPercent >= 0 ? "+" : ""}${plPercent.toFixed(2)}%)</span>
${statusIcon ? `<span class="swing-pos-status">${statusIcon}</span>` : ""}
</div>
</div>
<div class="swing-pos-details">
<div class="swing-pos-row">
<span>Entry: $${pos.entryPrice.toFixed(2)}</span>
<span>Current: $${currentPrice.toFixed(2)}</span>
<span>${pos.shares.toLocaleString()} shares</span>
</div>
<div class="swing-pos-row">
<span class="swing-stop">Stop: $${pos.stopPrice.toFixed(2)} (-${pos.stopPercent}%)</span>
<span class="swing-target">Target: $${pos.targetPrice.toFixed(2)} (+${pos.targetPercent}%)</span>
</div>
<div class="swing-pos-progress">
<div class="swing-progress-bar">
<div class="swing-progress-fill" style="width:${progressPercent}%"></div>
<div class="swing-progress-marker" style="left:${progressPercent}%"></div>
</div>
<div class="swing-progress-labels">
<span>Stop</span>
<span>Target</span>
</div>
</div>
<div class="swing-pos-meta">
<span>Opened: ${entryDate} (${holdDays}d)</span>
<span>Value: ${formatMoney(value)}</span>
</div>
</div>
<div class="swing-pos-actions">
<button class="swing-close-btn" data-id="${pos.id}" data-sym="${pos.symbol}">Close Position</button>
<button class="swing-merge-btn" data-id="${pos.id}" title="Add shares to this position">Merge</button>
<button class="swing-edit-btn" data-id="${pos.id}">Edit</button>
</div>
</div>`;
}).join("");
const summaryHtml = `
<div class="swing-positions-summary">
<div class="swing-summary-item">
<span class="swing-summary-label">Active Positions</span>
<span class="swing-summary-value">${positions.length}</span>
</div>
<div class="swing-summary-item ${totalPL >= 0 ? "positive" : "negative"}">
<span class="swing-summary-label">Open P&L</span>
<span class="swing-summary-value">${totalPL >= 0 ? "+" : ""}${formatMoney(totalPL)}</span>
</div>
<div class="swing-summary-item">
<span class="swing-summary-label">Total Value</span>
<span class="swing-summary-value">${formatMoney(totalValue)}</span>
</div>
</div>`;
return summaryHtml + positionRows;
}
function renderSwingSignals(opportunities = null) {
if (!opportunities) {
return `<div class="swing-empty">
<div style="font-size:24px; margin-bottom:8px;">\u{1F50D}</div>
<div>Click "Scan" to analyze vault stocks</div>
</div>`;
}
const signalColors = {
"strong_buy": "#00e676",
"buy": "#66bb6a",
"watch": "#ffd54f",
"neutral": "#888",
"take_profit": "#ffb74d",
"sell": "#ff7043",
"strong_sell": "#ef5350"
};
const signalLabels = {
"strong_buy": "\u{1F7E2} STRONG BUY",
"buy": "\u{1F7E2} BUY",
"watch": "\u{1F7E1} WATCH",
"neutral": "\u26AA NEUTRAL",
"take_profit": "\u{1F7E0} TAKE PROFIT",
"sell": "\u{1F534} SELL",
"strong_sell": "\u{1F534} STRONG SELL"
};
const interesting = opportunities.filter((o) => o.signal !== "neutral");
if (interesting.length === 0) {
return `<div class="swing-empty">
<div style="font-size:24px; margin-bottom:8px;">\u{1F634}</div>
<div>No strong signals right now</div>
<div style="font-size:10px; color:#666; margin-top:5px;">Vault stocks only \u2014 add stocks in Settings \u2192 Vault</div>
</div>`;
}
const buySignals = interesting.filter((o) => o.score > 0).slice(0, 8);
const sellSignals = interesting.filter((o) => o.score < 0).slice(0, 8);
let html = "";
if (buySignals.length > 0) {
html += `<div class="swing-signal-group">
<div class="swing-signal-group-title" style="color:#66bb6a;">\u{1F4C8} Buy Opportunities</div>
${buySignals.map((sig) => `
<div class="swing-signal-card buy" data-sym="${sig.symbol}">
<div class="swing-signal-header">
<span class="swing-signal-symbol">${sig.symbol}</span>
<span class="swing-signal-badge" style="background:${signalColors[sig.signal]}22; color:${signalColors[sig.signal]};">${signalLabels[sig.signal]}</span>
<span class="swing-signal-score">Score: ${sig.score}</span>
</div>
<div class="swing-signal-price">$${sig.price.toFixed(2)}</div>
<div class="swing-signal-reasons">
${sig.signals.filter((s) => s.direction === "bullish").map((s) => `<span class="swing-reason bullish">\u2713 ${s.message}</span>`).join("")}
</div>
<div class="swing-signal-targets">
<span>Target: +${sig.suggestedTarget.toFixed(1)}% ($${(sig.price * (1 + sig.suggestedTarget / 100)).toFixed(2)})</span>
<span>Stop: -${sig.suggestedStop.toFixed(1)}% ($${(sig.price * (1 - sig.suggestedStop / 100)).toFixed(2)})</span>
</div>
<div class="swing-signal-actions">
${!sig.hasActivePosition ? `<button class="swing-signal-track-btn" data-sym="${sig.symbol}" data-price="${sig.price}" data-target="${sig.suggestedTarget}" data-stop="${sig.suggestedStop}">Track Position</button>` : '<span class="swing-already-tracking">Already tracking</span>'}
</div>
</div>`).join("")}
</div>`;
}
if (sellSignals.length > 0) {
html += `<div class="swing-signal-group">
<div class="swing-signal-group-title" style="color:#ef5350;">\u{1F4C9} Take Profit / Exit Signals</div>
${sellSignals.map((sig) => `
<div class="swing-signal-card sell" data-sym="${sig.symbol}">
<div class="swing-signal-header">
<span class="swing-signal-symbol">${sig.symbol}</span>
<span class="swing-signal-badge" style="background:${signalColors[sig.signal]}22; color:${signalColors[sig.signal]};">${signalLabels[sig.signal]}</span>
<span class="swing-signal-score">Score: ${sig.score}</span>
</div>
<div class="swing-signal-price">$${sig.price.toFixed(2)}</div>
<div class="swing-signal-reasons">
${sig.signals.filter((s) => s.direction === "bearish").map((s) => `<span class="swing-reason bearish">\u26A0 ${s.message}</span>`).join("")}
</div>
${sig.currentShares > 0 ? `<div class="swing-signal-holding">You own ${sig.currentShares.toLocaleString()} shares</div>` : ""}
</div>`).join("")}
</div>`;
}
return html;
}
function renderSwingStats() {
const stats = getSwingTradeStats("all");
if (stats.totalTrades === 0) {
return `<div class="swing-stats-empty">No completed trades yet</div>`;
}
return `
<div class="swing-stats-grid">
<div class="swing-stat-card">
<span class="swing-stat-label">Total Trades</span>
<span class="swing-stat-value">${stats.totalTrades}</span>
</div>
<div class="swing-stat-card ${stats.winRate >= 50 ? "positive" : "negative"}">
<span class="swing-stat-label">Win Rate</span>
<span class="swing-stat-value">${stats.winRate.toFixed(1)}%</span>
<span class="swing-stat-sub">${stats.winners}W / ${stats.losers}L</span>
</div>
<div class="swing-stat-card ${stats.totalPnl >= 0 ? "positive" : "negative"}">
<span class="swing-stat-label">Total P&L</span>
<span class="swing-stat-value">${stats.totalPnl >= 0 ? "+" : ""}${formatMoneyWhole(stats.totalPnl)}</span>
</div>
<div class="swing-stat-card">
<span class="swing-stat-label">Avg P&L</span>
<span class="swing-stat-value ${stats.avgPnl >= 0 ? "positive" : "negative"}">${stats.avgPnl >= 0 ? "+" : ""}${formatMoneyWhole(stats.avgPnl)}</span>
<span class="swing-stat-sub">(${stats.avgPnlPercent >= 0 ? "+" : ""}${stats.avgPnlPercent.toFixed(2)}%)</span>
</div>
<div class="swing-stat-card">
<span class="swing-stat-label">Avg Hold</span>
<span class="swing-stat-value">${stats.avgHoldDays.toFixed(1)} days</span>
</div>
${stats.bestTrade ? `
<div class="swing-stat-card positive">
<span class="swing-stat-label">Best Trade</span>
<span class="swing-stat-value">${stats.bestTrade.symbol}</span>
<span class="swing-stat-sub">+${formatMoneyWhole(stats.bestTrade.pnl)}</span>
</div>
` : ""}
</div>`;
}
const JOURNAL_PRESET_TAGS = ["swing", "scalp", "mistake", "lesson", "rebalance", "vault"];
function renderSwingJournalEntry(trade, editingTradeId = null) {
const isEditing = editingTradeId === trade.id;
const entryDate = new Date(trade.entryDate).toLocaleDateString();
const exitDate = new Date(trade.exitDate).toLocaleDateString();
const exitReasonLabels = { "target_hit": "\u{1F3AF} Target", "stopped": "\u{1F6D1} Stopped", "manual_close": "\u270B Manual" };
const tags = trade.tags || [];
if (isEditing) {
return `
<div class="swing-journal-entry swing-journal-entry-editing ${trade.pnl >= 0 ? "winner" : "loser"}" data-id="${trade.id}">
<div class="swing-journal-inline-edit">
<div class="swing-journal-inline-row">
<span class="swing-journal-symbol">${trade.symbol}</span>
<div class="swing-journal-inline-fields">
<label>Entry $</label><input type="number" class="swing-inline-input" data-field="entryPrice" step="0.01" value="${trade.entryPrice}">
<label>Exit $</label><input type="number" class="swing-inline-input" data-field="exitPrice" step="0.01" value="${trade.exitPrice}">
<label>Shares</label><input type="number" class="swing-inline-input" data-field="shares" value="${trade.shares}">
</div>
</div>
<div class="swing-journal-inline-actions">
<button class="swing-inline-save alfa-mini-btn" style="border-color:#66bb6a; color:#66bb6a;">Save</button>
<button class="swing-inline-cancel alfa-mini-btn" style="border-color:#888; color:#888;">Cancel</button>
</div>
</div>
</div>`;
}
const notesRaw = trade.notes || "";
const notesPreview = notesRaw ? notesRaw.length > 40 ? notesRaw.substring(0, 40) + "\u2026" : notesRaw : "";
const notesEscaped = notesPreview.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
return `
<div class="swing-journal-entry ${trade.pnl >= 0 ? "winner" : "loser"}" data-id="${trade.id}">
<div class="swing-journal-header">
<span class="swing-journal-symbol">${trade.symbol}</span>
<div class="swing-journal-header-right">
<span class="swing-journal-pnl ${trade.pnl >= 0 ? "positive" : "negative"}">${trade.pnl >= 0 ? "+" : ""}${formatMoneyWhole(trade.pnl)} (${trade.pnlPercent >= 0 ? "+" : ""}${trade.pnlPercent.toFixed(2)}%)</span>
<div class="swing-journal-actions">
<button class="swing-journal-edit-btn" data-id="${trade.id}" title="Quick edit">\u270F\uFE0F</button>
<button class="swing-journal-full-edit-btn" data-id="${trade.id}" title="Full edit">\u{1F4DD}</button>
<button class="swing-journal-delete-btn" data-id="${trade.id}" title="Delete">\u{1F5D1}\uFE0F</button>
</div>
</div>
</div>
<div class="swing-journal-details">
<span>Entry: $${trade.entryPrice.toFixed(2)} \u2192 Exit: $${trade.exitPrice.toFixed(2)}</span>
<span>${trade.shares.toLocaleString()} shares</span>
</div>
<div class="swing-journal-meta">
<span>${entryDate} \u2192 ${exitDate} (${trade.holdingDays}d)</span>
<span>${exitReasonLabels[trade.exitReason] || trade.exitReason}</span>
${trade.fee > 0 ? `<span style="color:#ff9800;">Fee: ${formatMoneyWhole(trade.fee)}</span>` : ""}
</div>
${tags.length ? `<div class="swing-journal-tags">${tags.map((t) => `<span class="swing-journal-tag">${(t || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}</span>`).join("")}</div>` : ""}
${notesPreview ? `<div class="swing-journal-notes-preview" title="${(trade.notes || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)}">${notesEscaped}</div>` : ""}
</div>`;
}
function renderSwingJournal(editingTradeId = null) {
const trades = swingTradeData.completedTrades.slice(0, 20);
if (trades.length === 0) {
return `<div class="swing-empty">
<div style="font-size:24px; margin-bottom:8px;">\u{1F4D3}</div>
<div>No completed trades</div>
</div>`;
}
return trades.map((trade) => renderSwingJournalEntry(trade, editingTradeId)).join("");
}
function bindSwingPositionActions() {
$(".swing-close-btn").off("click").on("click", function() {
const id = $(this).data("id");
const pos = swingTradeData.activePositions.find((p) => p.id === id);
if (pos) openClosePositionModal(pos);
});
$(".swing-merge-btn").off("click").on("click", function() {
openMergeIntoPositionModal($(this).data("id"));
});
$(".swing-edit-btn").off("click").on("click", function() {
const id = $(this).data("id");
openEditPositionModal(id);
});
}
function bindSwingJournalActions(editingTradeId = null) {
$(".swing-journal-edit-btn").off("click").on("click", function() {
const id = $(this).data("id");
$("#swing-journal-list").html(renderSwingJournal(id));
bindSwingJournalActions(id);
});
$(".swing-journal-full-edit-btn").off("click").on("click", function() {
openEditJournalEntryModal($(this).data("id"));
});
$(".swing-journal-delete-btn").off("click").on("click", function() {
const id = $(this).data("id");
const trade = swingTradeData.completedTrades.find((t) => t.id === id);
if (trade && confirm(`Delete journal entry for ${trade.symbol}?
P&L: ${formatMoney(trade.pnl)}`)) {
deleteJournalEntry(id);
refreshSwingJournal();
}
});
$(".swing-inline-save").off("click").on("click", function() {
const entry = $(this).closest(".swing-journal-entry");
const id = entry.data("id");
const trade = swingTradeData.completedTrades.find((t) => t.id === id);
if (!trade) return;
trade.entryPrice = parseFloat(entry.find('[data-field="entryPrice"]').val()) || trade.entryPrice;
trade.exitPrice = parseFloat(entry.find('[data-field="exitPrice"]').val()) || trade.exitPrice;
trade.shares = parseInt(entry.find('[data-field="shares"]').val()) || trade.shares;
recalculateCompletedTrade(trade);
saveSwingTradeData();
refreshSwingJournal();
});
$(".swing-inline-cancel").off("click").on("click", function() {
refreshSwingJournal();
});
}
function refreshSwingJournal() {
$("#swing-stats").html(renderSwingStats());
$("#swing-journal-list").html(renderSwingJournal());
bindSwingJournalActions();
}
function openEditJournalEntryModal(tradeId) {
const trade = swingTradeData.completedTrades.find((t) => t.id === tradeId);
if (!trade) return;
const tags = trade.tags || [];
const tagsStr = tags.join(", ");
const exitReasonOptions = [
{ v: "target_hit", l: "\u{1F3AF} Target" },
{ v: "stopped", l: "\u{1F6D1} Stopped" },
{ v: "manual_close", l: "\u270B Manual" }
].map((o) => `<option value="${o.v}" ${trade.exitReason === o.v ? "selected" : ""}>${o.l}</option>`).join("");
const html = `
<div class="add-position-form">
<div class="form-group">
<label>Stock: ${trade.symbol}</label>
<input type="hidden" id="journal-edit-id" value="${trade.id}">
</div>
<div class="form-row">
<div class="form-group">
<label>Entry Price ($)</label>
<input type="number" id="journal-entry-price" class="alfa-input" step="0.01" value="${trade.entryPrice}">
</div>
<div class="form-group">
<label>Exit Price ($)</label>
<input type="number" id="journal-exit-price" class="alfa-input" step="0.01" value="${trade.exitPrice}">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Shares</label>
<input type="number" id="journal-shares" class="alfa-input" value="${trade.shares}">
</div>
<div class="form-group">
<label>Exit Reason</label>
<select id="journal-exit-reason" class="alfa-select">${exitReasonOptions}</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Entry Date</label>
<input type="text" id="journal-entry-date" class="alfa-input" value="${new Date(trade.entryDate).toISOString().slice(0, 10)}" placeholder="YYYY-MM-DD">
</div>
<div class="form-group">
<label>Exit Date</label>
<input type="text" id="journal-exit-date" class="alfa-input" value="${new Date(trade.exitDate).toISOString().slice(0, 10)}" placeholder="YYYY-MM-DD">
</div>
</div>
<div class="form-group">
<label>Tags (comma-separated)</label>
<input type="text" id="journal-tags" class="alfa-input" value="${tagsStr}" placeholder="swing, lesson, mistake">
<span class="form-hint">Presets: ${JOURNAL_PRESET_TAGS.join(", ")}</span>
</div>
<div class="form-group">
<label>Notes</label>
<textarea id="journal-notes" class="alfa-input journal-notes-textarea" rows="4" placeholder="Lessons learned...">${(trade.notes || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}</textarea>
</div>
<div class="form-actions">
<button id="journal-edit-save" class="alfa-main-btn" style="border-color:#caa14a; color:#caa14a; flex:1;">Save</button>
<button id="journal-edit-cancel" class="alfa-main-btn" style="border-color:#888; color:#888;">Cancel</button>
</div>
</div>`;
createModal("\u270F\uFE0F Edit Journal Entry", html);
$("#journal-edit-save").on("click", function() {
const id = $("#journal-edit-id").val();
const t = swingTradeData.completedTrades.find((x) => x.id === id);
if (!t) return;
t.entryPrice = parseFloat($("#journal-entry-price").val()) || t.entryPrice;
t.exitPrice = parseFloat($("#journal-exit-price").val()) || t.exitPrice;
t.shares = parseInt($("#journal-shares").val()) || t.shares;
t.exitReason = $("#journal-exit-reason").val() || t.exitReason;
const ed = $("#journal-entry-date").val();
const exd = $("#journal-exit-date").val();
if (ed) t.entryDate = new Date(ed).getTime();
if (exd) t.exitDate = new Date(exd).getTime();
t.notes = $("#journal-notes").val().trim();
const tagsInput = $("#journal-tags").val().trim();
t.tags = tagsInput ? tagsInput.split(",").map((s) => s.trim()).filter(Boolean) : [];
recalculateCompletedTrade(t);
saveSwingTradeData();
closeModal();
refreshSwingJournal();
});
$("#journal-edit-cancel").on("click", closeModal);
}
function bindSwingSignalActions() {
$(".swing-signal-track-btn").off("click").on("click", function() {
const sym = $(this).data("sym");
const price = parseFloat($(this).data("price"));
const target = parseFloat($(this).data("target"));
const stop = parseFloat($(this).data("stop"));
openAddPositionModal(sym, price, target, stop);
});
}
function updateSignalCardsForTrackedSymbol(symbol) {
$(".swing-signal-card.buy[data-sym='" + symbol + "']").each(function() {
const $btn = $(this).find(".swing-signal-track-btn");
if ($btn.length) {
$btn.replaceWith('<span class="swing-already-tracking">Already tracking</span>');
}
});
}
function openAddPositionModal(prefillSym = "", prefillPrice = 0, prefillTarget = 3, prefillStop = 2) {
const stockOptions = Object.keys(STOCK_DATA).sort().map(
(sym) => `<option value="${sym}" ${sym === prefillSym ? "selected" : ""}>${sym}</option>`
).join("");
let currentPrice = prefillPrice;
if (prefillSym && !currentPrice) {
currentPrice = getPrice(prefillSym) || 0;
}
let currentShares = 0;
if (prefillSym) {
currentShares = getOwnedShares(prefillSym);
}
const html = `
<div class="add-position-form">
<div class="form-group">
<label>Stock Symbol</label>
<select id="pos-symbol" class="alfa-select">
<option value="">Select Stock...</option>
${stockOptions}
</select>
</div>
<div id="pos-merge-notice" class="pos-merge-notice" style="display:none;">
<span>You already have an open position for <strong id="pos-merge-sym"></strong>.</span>
<div class="pos-merge-buttons">
<button id="pos-merge-btn" class="alfa-mini-btn" style="border-color:#caa14a; color:#caa14a;">Add to existing</button>
<button id="pos-create-new-btn" class="alfa-mini-btn" style="border-color:#888; color:#888;">Create new position</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Entry Price ($)</label>
<input type="number" id="pos-entry-price" class="alfa-input" step="0.01" value="${currentPrice.toFixed(2)}" placeholder="0.00">
<span class="form-hint" id="pos-current-price">Current: $${currentPrice.toFixed(2)}</span>
</div>
<div class="form-group">
<label id="pos-shares-label">Shares</label>
<input type="number" id="pos-shares" class="alfa-input" value="${currentShares}" placeholder="0">
<span class="form-hint" id="pos-owned-shares">Owned: ${currentShares.toLocaleString()}</span>
</div>
</div>
<div class="form-group pos-source-modes">
<label class="quick-trade-label">Position source</label>
<div class="pos-source-options">
<label class="pos-source-option"><input type="radio" name="pos-source" value="track" ${currentShares > 0 ? "checked" : ""}> Track owned</label>
<label class="pos-source-option"><input type="radio" name="pos-source" value="buy" ${currentShares > 0 ? "" : "checked"}> Buy with cash</label>
<label class="pos-source-option"><input type="radio" name="pos-source" value="track_buy"> Track + buy more</label>
</div>
<div id="pos-buy-extra-wrap" class="form-group" style="display:none; margin-top:4px;">
<label id="pos-buy-amount-label">Amount to buy ($)</label>
<input type="text" id="pos-buy-amount" class="alfa-input" value="" placeholder="e.g. 100k, 1m">
<span class="form-hint" id="pos-cash-hint">Cash: -- \u2248 0 shares</span>
</div>
<div id="pos-trade-status" class="gamble-status" style="margin-top:6px;"></div>
</div>
<div class="form-row">
<div class="form-group">
<label id="pos-target-label">Target % Gain</label>
<div class="form-row" style="gap:8px; align-items:center;">
<input type="number" id="pos-target" class="alfa-input" step="0.01" value="${prefillTarget}" placeholder="3" style="flex:1;">
<select id="pos-target-unit" class="alfa-select" style="width:56px;">
<option value="pct">%</option>
<option value="dol">$</option>
</select>
</div>
<span class="form-hint" id="pos-target-hint">Target: $${(currentPrice * (1 + prefillTarget / 100)).toFixed(2)}</span>
</div>
<div class="form-group">
<label id="pos-stop-label">Stop % Loss</label>
<div class="form-row" style="gap:8px; align-items:center;">
<input type="number" id="pos-stop" class="alfa-input" step="0.01" value="${prefillStop}" placeholder="2" style="flex:1;">
<select id="pos-stop-unit" class="alfa-select" style="width:56px;">
<option value="pct">%</option>
<option value="dol">$</option>
</select>
</div>
<span class="form-hint" id="pos-stop-hint">Stop: $${(currentPrice * (1 - prefillStop / 100)).toFixed(2)}</span>
</div>
</div>
<div class="form-group">
<label>Notes (optional)</label>
<textarea id="pos-notes" class="alfa-input journal-notes-textarea" rows="2" placeholder="Why are you taking this trade?"></textarea>
</div>
<div class="form-summary" id="pos-summary">
<div class="summary-row">
<span>Position Value:</span>
<span id="pos-value">$${(currentPrice * currentShares).toLocaleString("en-US", { maximumFractionDigits: 2 })}</span>
</div>
<div class="summary-row" id="pos-buy-cost-row" style="display:none;">
<span>Buy Cost:</span>
<span id="pos-buy-cost">$0</span>
</div>
<div class="summary-row">
<span>Potential Profit:</span>
<span id="pos-potential-profit" class="positive">+$${(currentPrice * (prefillTarget / 100) * currentShares).toFixed(2)}</span>
</div>
<div class="summary-row">
<span>Max Loss:</span>
<span id="pos-max-loss" class="negative">-$${(currentPrice * (prefillStop / 100) * currentShares).toFixed(2)}</span>
</div>
<div class="summary-row">
<span>Risk/Reward:</span>
<span id="pos-risk-reward">${(prefillTarget / prefillStop).toFixed(2)}:1</span>
</div>
</div>
<div class="form-actions">
<button id="pos-submit" class="alfa-main-btn" style="border-color:#66bb6a; color:#66bb6a; flex:1;">Add Position</button>
<button id="pos-cancel" class="alfa-main-btn" style="border-color:#888; color:#888;">Cancel</button>
</div>
</div>`;
createModal("\u{1F4CA} Add Swing Position", html);
(function enhancePosSymbolSelect() {
const $sel = $("#pos-symbol");
if (!$sel.length || $sel.data("enhanced")) return;
$sel.data("enhanced", true);
const $wrap = $('<div class="pos-symbol-wrap"></div>');
$sel.before($wrap);
$wrap.append($sel.addClass("pos-symbol-native"));
const selectedLabel = () => {
const t = $sel.find("option:selected").text();
return t || "Select Stock...";
};
const $btn = $(`<button type="button" class="alfa-select pos-symbol-trigger">${selectedLabel()}</button>`);
const $menu = $('<div class="pos-symbol-menu" hidden></div>');
$wrap.append($btn, $menu);
function rebuildMenu() {
$menu.empty();
$sel.find("option").each(function() {
const val = String(this.value);
const text = $(this).text();
const $item = $(`<button type="button" class="pos-symbol-option" data-value="${val}"></button>`).text(text);
if (val === String($sel.val() || "")) $item.addClass("is-selected");
$menu.append($item);
});
}
function closeMenu() {
$menu.prop("hidden", true);
$btn.attr("aria-expanded", "false");
}
function openMenu() {
rebuildMenu();
$menu.prop("hidden", false);
$btn.attr("aria-expanded", "true");
}
$btn.on("click", function(e) {
e.preventDefault();
e.stopPropagation();
if ($menu.prop("hidden")) openMenu();
else closeMenu();
});
$menu.on("click", ".pos-symbol-option", function(e) {
e.preventDefault();
e.stopPropagation();
const val = String($(this).data("value") ?? "");
$sel.val(val).trigger("change");
$btn.text($(this).text());
closeMenu();
});
$("#alfa-modal-overlay").on("mousedown.posSymbol", function(e) {
if (!$(e.target).closest(".pos-symbol-wrap").length) closeMenu();
});
})();
function checkMergeNotice() {
const sym = $("#pos-symbol").val();
const existing = swingTradeData.activePositions.find((p) => p.symbol === sym);
if (existing) {
$("#pos-merge-notice").show();
$("#pos-merge-sym").text(sym);
} else {
$("#pos-merge-notice").hide();
}
}
$("#pos-merge-btn").on("click", function() {
const sym = $("#pos-symbol").val();
const existing = swingTradeData.activePositions.find((p) => p.symbol === sym);
if (existing) {
closeModal();
openMergeIntoPositionModal(existing.id);
}
});
$("#pos-create-new-btn").on("click", function() {
$("#pos-merge-notice").hide();
});
function getPosSourceMode() {
return $("input[name='pos-source']:checked").val() || "track";
}
function syncPosSourceUI(opts2 = {}) {
const mode = getPosSourceMode();
const sym = $("#pos-symbol").val();
const owned = sym ? getOwnedShares(sym) : 0;
const cash = getMoneyFast() || 0;
$("#pos-owned-shares").text(`Owned: ${owned.toLocaleString()}`);
$("#pos-cash-hint").text(`Cash: ${formatMoney(cash)} \u2248 0 shares`);
if (mode === "track") {
$("#pos-shares-label").text("Shares to track");
$("#pos-shares").closest(".form-group").show();
$("#pos-buy-extra-wrap").hide();
$("#pos-buy-cost-row").hide();
$("#pos-submit").text("Add Position");
if (opts2.resetShares) $("#pos-shares").val(owned > 0 ? owned : 0);
} else if (mode === "buy") {
$("#pos-shares").closest(".form-group").hide();
$("#pos-buy-extra-wrap").show();
$("#pos-buy-cost-row").show();
$("#pos-submit").text("Buy & Add");
if (opts2.resetShares) $("#pos-buy-amount").val(cash > 0 ? String(Math.round(cash)) : "");
} else {
$("#pos-shares-label").text("Shares to track");
$("#pos-shares").closest(".form-group").show();
$("#pos-buy-extra-wrap").show();
$("#pos-buy-cost-row").show();
$("#pos-submit").text("Buy & Add");
if (opts2.resetShares) {
$("#pos-shares").val(owned > 0 ? owned : 0);
$("#pos-buy-amount").val("");
}
}
updatePositionSummary();
}
$("#pos-symbol").on("change", function() {
const sym = $(this).val();
if (sym) {
const price = getPrice(sym) || 0;
const owned = getOwnedShares(sym);
$("#pos-entry-price").val(price.toFixed(2));
$("#pos-current-price").text(`Current: $${price.toFixed(2)}`);
$("input[name='pos-source'][value='" + (owned > 0 ? "track" : "buy") + "']").prop("checked", true);
syncPosSourceUI({ resetShares: true });
checkMergeNotice();
} else {
$("#pos-merge-notice").hide();
}
});
checkMergeNotice();
syncPosSourceUI({ resetShares: false });
$("input[name='pos-source']").on("change", function() {
syncPosSourceUI({ resetShares: true });
});
$("#pos-entry-price, #pos-shares, #pos-buy-amount, #pos-target, #pos-stop, #pos-target-unit, #pos-stop-unit").on("input change", updatePositionSummary);
function getTargetStopPrices() {
const price = parseFloat($("#pos-entry-price").val()) || 0;
const targetVal = parseFloat($("#pos-target").val());
const stopVal = parseFloat($("#pos-stop").val());
const targetUnit = $("#pos-target-unit").val();
const stopUnit = $("#pos-stop-unit").val();
let targetPrice, stopPrice, targetPercent, stopPercent;
if (targetUnit === "dol" && targetVal != null && !isNaN(targetVal)) {
targetPrice = targetVal;
targetPercent = price > 0 ? (targetPrice / price - 1) * 100 : 0;
} else {
targetPercent = targetVal != null && !isNaN(targetVal) ? targetVal : 3;
targetPrice = price * (1 + targetPercent / 100);
}
if (stopUnit === "dol" && stopVal != null && !isNaN(stopVal)) {
stopPrice = stopVal;
stopPercent = price > 0 ? (1 - stopPrice / price) * 100 : 0;
} else {
stopPercent = stopVal != null && !isNaN(stopVal) ? stopVal : 2;
stopPrice = price * (1 - stopPercent / 100);
}
return { price, targetPrice, stopPrice, targetPercent, stopPercent };
}
function resolvePosBuyShares(tradePrice) {
const mode = getPosSourceMode();
if (mode !== "buy" && mode !== "track_buy") return 0;
if (tradePrice <= 0) return 0;
const raw = String($("#pos-buy-amount").val() || "").trim();
if (!raw) return 0;
const dollars = parseTornNumber(raw);
if (!dollars || dollars <= 0 || isNaN(dollars)) return 0;
return Math.floor(dollars / tradePrice);
}
function updatePositionSummary() {
const { price, targetPrice, stopPrice, targetPercent, stopPercent } = getTargetStopPrices();
const mode = getPosSourceMode();
const trackShares = parseInt($("#pos-shares").val(), 10) || 0;
const tradePrice = getPriceForTrade($("#pos-symbol").val()) || price;
const buyShares = resolvePosBuyShares(tradePrice);
const totalShares = mode === "buy" ? buyShares : mode === "track_buy" ? trackShares + buyShares : trackShares;
const value = tradePrice * totalShares;
const potentialProfit = (targetPrice - tradePrice) * totalShares;
const maxLoss = (tradePrice - stopPrice) * totalShares;
const riskReward = tradePrice - stopPrice > 0 ? (targetPrice - tradePrice) / (tradePrice - stopPrice) : 0;
const buyCost = buyShares * tradePrice;
$("#pos-target-hint").text($("#pos-target-unit").val() === "pct" ? `Target: $${targetPrice.toFixed(2)}` : `Target: ${targetPercent.toFixed(2)}%`);
$("#pos-stop-hint").text($("#pos-stop-unit").val() === "pct" ? `Stop: $${stopPrice.toFixed(2)}` : `Stop: ${stopPercent.toFixed(2)}%`);
$("#pos-value").text(`$${value.toLocaleString("en-US", { maximumFractionDigits: 2 })}`);
$("#pos-buy-cost").text(formatMoney(buyCost));
$("#pos-potential-profit").text(`+$${potentialProfit.toFixed(2)}`);
$("#pos-max-loss").text(`-$${maxLoss.toFixed(2)}`);
$("#pos-risk-reward").text(`${riskReward.toFixed(2)}:1`);
const cash = getMoneyFast() || 0;
$("#pos-cash-hint").text(`Cash: ${formatMoney(cash)} \u2248 ${buyShares.toLocaleString()} shares`);
}
$("#pos-target-unit").on("change", function() {
const price = parseFloat($("#pos-entry-price").val()) || 0;
const targetVal = parseFloat($("#pos-target").val());
if (price <= 0) return;
if ($(this).val() === "dol") {
const pct = targetVal != null && !isNaN(targetVal) ? targetVal : 3;
$("#pos-target").val((price * (1 + pct / 100)).toFixed(2));
} else {
const dol = targetVal != null && !isNaN(targetVal) ? targetVal : price * 1.03;
$("#pos-target").val(price > 0 ? ((dol / price - 1) * 100).toFixed(2) : 3);
}
updatePositionSummary();
});
$("#pos-stop-unit").on("change", function() {
const price = parseFloat($("#pos-entry-price").val()) || 0;
const stopVal = parseFloat($("#pos-stop").val());
if (price <= 0) return;
if ($(this).val() === "dol") {
const pct = stopVal != null && !isNaN(stopVal) ? stopVal : 2;
$("#pos-stop").val((price * (1 - pct / 100)).toFixed(2));
} else {
const dol = stopVal != null && !isNaN(stopVal) ? stopVal : price * 0.98;
$("#pos-stop").val(price > 0 ? ((1 - dol / price) * 100).toFixed(2) : 2);
}
updatePositionSummary();
});
$("#pos-submit").on("click", async function() {
const sym = $("#pos-symbol").val();
const entryInput = parseFloat($("#pos-entry-price").val()) || 0;
const mode = getPosSourceMode();
const trackShares = parseInt($("#pos-shares").val(), 10) || 0;
const notes = $("#pos-notes").val().trim();
const { targetPrice, stopPrice, targetPercent, stopPercent } = getTargetStopPrices();
const $btn = $(this);
const $status = $("#pos-trade-status");
if (!sym) {
alert("Please select a stock");
return;
}
if (entryInput <= 0) {
alert("Please enter a valid entry price");
return;
}
let finalShares = 0;
let finalEntry = entryInput;
if (mode === "track") {
if (trackShares <= 0) {
alert("Please enter number of shares to track");
return;
}
const owned = getOwnedShares(sym);
if (trackShares > owned) {
alert("You only own " + owned.toLocaleString() + " shares. Switch to Buy, or lower shares.");
return;
}
finalShares = trackShares;
finalEntry = entryInput;
} else if (mode === "buy") {
const tradePrice = getPriceForTrade(sym);
if (tradePrice <= 0) {
alert("Price unavailable");
return;
}
const buyShares = resolvePosBuyShares(tradePrice);
if (buyShares <= 0) {
alert("Enter a dollar amount to buy (e.g. 100k)");
return;
}
const cash = getMoneyFast() || 0;
const cost = buyShares * tradePrice;
if (cost > cash) {
alert("Insufficient cash. Need " + formatMoney(cost - cash) + " more.");
return;
}
$btn.prop("disabled", true).text("Buying...");
$status.html('<span style="color:#ffb74d;">Buying ' + buyShares.toLocaleString() + " shares...</span>");
try {
await postTradeAsync(sym, buyShares, "buyShares");
finalShares = buyShares;
finalEntry = tradePrice;
} catch (e) {
console.error("Swing buy error:", e);
$status.html('<span style="color:#ef5350;">Buy failed: ' + (e.message || e) + "</span>");
$btn.prop("disabled", false).text("Buy & Add");
return;
}
} else {
const tradePrice = getPriceForTrade(sym) || entryInput;
const buyShares = resolvePosBuyShares(tradePrice);
if (trackShares <= 0 && buyShares <= 0) {
alert("Enter shares to track and/or a buy amount");
return;
}
if (trackShares > 0) {
const owned = getOwnedShares(sym);
if (trackShares > owned) {
alert("You only own " + owned.toLocaleString() + " shares to track.");
return;
}
}
if (buyShares > 0) {
if (tradePrice <= 0) {
alert("Price unavailable");
return;
}
const cash = getMoneyFast() || 0;
const cost = buyShares * tradePrice;
if (cost > cash) {
alert("Insufficient cash. Need " + formatMoney(cost - cash) + " more.");
return;
}
$btn.prop("disabled", true).text("Buying...");
$status.html('<span style="color:#ffb74d;">Buying ' + buyShares.toLocaleString() + " shares...</span>");
try {
await postTradeAsync(sym, buyShares, "buyShares");
finalShares = trackShares + buyShares;
finalEntry = finalShares > 0 ? (trackShares * entryInput + buyShares * tradePrice) / finalShares : tradePrice;
} catch (e) {
console.error("Swing buy error:", e);
$status.html('<span style="color:#ef5350;">Buy failed: ' + (e.message || e) + "</span>");
$btn.prop("disabled", false).text("Buy & Add");
return;
}
} else {
finalShares = trackShares;
finalEntry = entryInput;
}
}
if (finalShares <= 0) {
alert("Please enter a valid amount");
return;
}
addSwingPosition(sym, finalEntry, finalShares, targetPercent, stopPercent, notes, targetPrice, stopPrice);
closeModal();
updateVaultDisplay(true);
$("#swing-positions-list").html(renderSwingPositions());
bindSwingPositionActions();
updateSignalCardsForTrackedSymbol(sym);
});
$("#pos-cancel").on("click", closeModal);
}
function getSwingBenefitLockedShares(sym, owned) {
const checkbox = $("#alfa-lock-toggle");
const lockBlocks = checkbox.length > 0 ? checkbox.is(":checked") : localStorage.getItem("alfa_vault_lock") === "true";
if (!lockBlocks) return 0;
const stockData = STOCK_DATA[sym];
if (!stockData || !owned) return 0;
const tierInfo = getBenefitTier(sym, owned);
if (stockData.type === "P") {
return owned >= stockData.base ? stockData.base : 0;
}
if (tierInfo.tier > 0) {
const minSharesForCurrentTier = stockData.base * (Math.pow(2, tierInfo.tier) - 1);
return Math.min(minSharesForCurrentTier, owned);
}
return 0;
}
function openClosePositionModal(pos) {
if (!pos) return;
const currentPrice = getPriceForTrade(pos.symbol) || getPrice(pos.symbol) || pos.currentPrice || pos.entryPrice;
const totalShares = pos.shares;
const owned = getOwnedShares(pos.symbol);
const benefitLocked = getSwingBenefitLockedShares(pos.symbol, owned);
const maxOwnedSellable = Math.max(0, owned);
const exitReasonOptions = [
{ v: "manual_close", l: "\u270B Manual" },
{ v: "target_hit", l: "\u{1F3AF} Target" },
{ v: "stopped", l: "\u{1F6D1} Stopped" }
].map((o) => `<option value="${o.v}">${o.l}</option>`).join("");
const html = `
<div class="quick-trade-container">
<div class="quick-trade-header">
<span class="quick-trade-symbol">${pos.symbol}</span>
<span class="quick-trade-price">${formatMoney(currentPrice)}</span>
</div>
<div class="quick-trade-info">
<div class="quick-trade-info-row">
<span>Entry Price</span>
<span>${formatMoney(pos.entryPrice)}</span>
</div>
<div class="quick-trade-info-row">
<span>Total Shares</span>
<span>${pos.shares.toLocaleString()}</span>
</div>
<div class="quick-trade-info-row">
<span>Gross Proceeds</span>
<span id="close-pos-gross">-</span>
</div>
<div class="quick-trade-info-row">
<span>Fee (0.1%)</span>
<span id="close-pos-fee" style="color:#888;">-</span>
</div>
</div>
<div class="quick-trade-calc">
<div class="quick-trade-calc-row highlight">
<span>Net P&L</span>
<span id="close-pos-pnl">-</span>
</div>
</div>
<div class="quick-trade-input-section">
<label class="quick-trade-label">Shares to close</label>
<div class="form-row" style="gap:8px; align-items:center;">
<input type="number" id="close-pos-shares" class="alfa-input" min="1" max="${totalShares}" value="${totalShares}" placeholder="${totalShares}" style="flex:1;">
<span class="form-hint" style="margin:0;">of ${totalShares.toLocaleString()}</span>
</div>
<span class="form-hint" id="close-pos-partial-hint" style="display:none;">Partial close: position will stay open with remaining shares.</span>
<div class="quick-trade-hint" id="close-pos-tier-hint" style="display:none;"></div>
</div>
<div class="quick-trade-input-section">
<label class="quick-trade-label">Exit Reason</label>
<select id="close-pos-exit-reason" class="alfa-select" style="width:100%;">${exitReasonOptions}</select>
</div>
<div class="quick-trade-input-section">
<label class="swing-sell-toggle">
<input type="checkbox" id="close-pos-sell" ${maxOwnedSellable > 0 ? "checked" : ""} ${maxOwnedSellable <= 0 ? "disabled" : ""}>
<span>Sell shares on close</span>
</label>
<div class="quick-trade-hint" id="close-pos-sell-hint">${maxOwnedSellable > 0 ? "Sells the full close amount at market, then closes tracking." : "No owned shares to sell. Uncheck sell to close tracking only."}</div>
</div>
<div id="close-pos-status" class="gamble-status" style="margin-top:8px;"></div>
<div class="quick-trade-actions">
<button id="close-pos-confirm" class="alfa-main-btn qb-btn-sell">Close & Sell</button>
<button id="close-pos-cancel" class="alfa-main-btn qb-btn-close">Cancel</button>
</div>
</div>`;
createModal(`Close ${pos.symbol} Position`, html);
function updateCloseSummary() {
let sh = parseInt($("#close-pos-shares").val(), 10);
if (isNaN(sh) || sh < 1) sh = 1;
if (sh > totalShares) sh = totalShares;
$("#close-pos-shares").val(sh);
const sellOn = $("#close-pos-sell").is(":checked");
const sellShares = sellOn ? Math.min(sh, maxOwnedSellable) : sh;
const grossProceeds = currentPrice * sellShares;
const fee = grossProceeds * 1e-3;
const netProceeds = grossProceeds - fee;
const cost = pos.entryPrice * sellShares;
const netPnl = netProceeds - cost;
const pnlPercent = cost > 0 ? (netProceeds - cost) / cost * 100 : 0;
const isPositive = netPnl >= 0;
$("#close-pos-gross").text(formatMoney(grossProceeds));
$("#close-pos-fee").text("-" + formatMoney(fee));
$("#close-pos-pnl").attr("style", "color:" + (isPositive ? "#8bc34a" : "#ef5350") + ";").text((isPositive ? "+" : "") + formatMoney(netPnl) + " (" + (pnlPercent >= 0 ? "+" : "") + pnlPercent.toFixed(1) + "%)");
if (sh < totalShares) {
$("#close-pos-partial-hint").show();
} else {
$("#close-pos-partial-hint").hide();
}
const dipsIntoTier = sellOn && benefitLocked > 0 && sellShares > Math.max(0, owned - benefitLocked);
if (dipsIntoTier) {
$("#close-pos-tier-hint").html(`\u26A0 Selling ${sellShares.toLocaleString()} may drop your benefit tier (tier needs ${benefitLocked.toLocaleString()} shares).`).show();
} else {
$("#close-pos-tier-hint").hide().text("");
}
if (sellOn && sellShares < sh) {
$("#close-pos-sell-hint").text(`You own ${owned.toLocaleString()} \u2014 will sell ${sellShares.toLocaleString()} and close that many.`);
} else if (sellOn) {
$("#close-pos-sell-hint").text(`Will sell all ${sellShares.toLocaleString()} shares at market, then close tracking.`);
} else {
$("#close-pos-sell-hint").text("Closes tracking only \u2014 no market sell.");
}
$("#close-pos-confirm").text(sellOn ? "Close & Sell" : "Close Position");
}
updateCloseSummary();
$("#close-pos-shares").on("input change", updateCloseSummary);
$("#close-pos-sell").on("change", updateCloseSummary);
$("#close-pos-confirm").on("click", async function() {
let sh = parseInt($("#close-pos-shares").val(), 10);
if (isNaN(sh) || sh < 1) {
alert("Enter at least 1 share to close.");
return;
}
if (sh > totalShares) {
alert("Shares to close cannot exceed " + totalShares.toLocaleString() + ".");
return;
}
const exitReason = $("#close-pos-exit-reason").val() || "manual_close";
const sellOn = $("#close-pos-sell").is(":checked");
const $btn = $(this);
const $status = $("#close-pos-status");
const $cancel = $("#close-pos-cancel");
if (sellOn) {
const toSell = Math.min(sh, maxOwnedSellable);
if (toSell <= 0) {
$status.html('<span style="color:#ef5350;">No owned shares to sell. Uncheck sell to close tracking only.</span>');
return;
}
$btn.prop("disabled", true).text("Selling...");
$cancel.prop("disabled", true);
$status.html('<span style="color:#ffb74d;">Placing sell order...</span>');
try {
await postTradeAsync(pos.symbol, toSell, "sellShares");
const sharesToClose2 = toSell >= totalShares ? null : toSell;
closeSwingPosition(pos.id, currentPrice, exitReason, sharesToClose2);
closeModal();
updateVaultDisplay(true);
$("#swing-positions-list").html(renderSwingPositions());
bindSwingPositionActions();
refreshSwingJournal();
} catch (e) {
console.error("Swing close sell error:", e);
$status.html(`<span style="color:#ef5350;">Sell failed: ${e.message || e}</span>`);
$btn.prop("disabled", false).text("Close & Sell");
$cancel.prop("disabled", false);
}
return;
}
const sharesToClose = sh >= totalShares ? null : sh;
closeModal();
closeSwingPosition(pos.id, null, exitReason, sharesToClose);
$("#swing-positions-list").html(renderSwingPositions());
bindSwingPositionActions();
refreshSwingJournal();
showUndoCloseBanner();
});
$("#close-pos-cancel").on("click", closeModal);
}
function openMergeIntoPositionModal(existingPositionId) {
const pos = swingTradeData.activePositions.find((p) => p.id === existingPositionId);
if (!pos) return;
const totalOwned = getOwnedShares(pos.symbol);
const looseShares = Math.max(0, totalOwned - pos.shares);
const costBasisPrice = getLooseSharesAvgPrice(pos.symbol, looseShares);
const currentPrice = getPrice(pos.symbol) || pos.entryPrice;
const prefillPrice = costBasisPrice || currentPrice;
const priceSource = costBasisPrice ? "cost basis" : "current (sync portfolio for cost basis)";
const html = `
<div class="add-position-form">
<div class="form-group">
<label>Merge into existing ${pos.symbol} position</label>
<div class="merge-existing-info">
Position: ${pos.shares.toLocaleString()} shares @ $${pos.entryPrice.toFixed(2)} (avg)
<br>Target: ${pos.targetPercent}% | Stop: ${pos.stopPercent}%
</div>
</div>
<div class="form-group merge-shares-context">
<span>You own <strong>${totalOwned.toLocaleString()}</strong> total</span>
<span>\u2192 <strong>${looseShares.toLocaleString()}</strong> loose shares available to merge</span>
</div>
${looseShares === 0 ? `
<div class="merge-no-loose">
No loose shares to merge. Your position (${pos.shares.toLocaleString()}) matches or exceeds total owned (${totalOwned.toLocaleString()}).
</div>
` : ""}
<div class="form-row">
<div class="form-group">
<label>Price per share ($)</label>
<input type="number" id="merge-price" class="alfa-input" step="0.01" value="${prefillPrice.toFixed(2)}">
<span class="form-hint" id="merge-price-hint">${costBasisPrice ? "From " + priceSource : priceSource}</span>
</div>
<div class="form-group">
<label>Shares to add</label>
<input type="number" id="merge-shares" class="alfa-input" value="${looseShares}" placeholder="0">
<button id="merge-use-all" class="alfa-mini-btn" style="border-color:#caa14a; color:#caa14a; margin-top:4px;">Use all loose</button>
</div>
</div>
<div class="form-group" id="merge-result" style="display:none;">
<label>New weighted average</label>
<div class="merge-result-value">
<span id="merge-new-avg">-</span> / <span id="merge-new-shares">-</span> shares
</div>
</div>
<div class="form-actions">
<button id="merge-submit" class="alfa-main-btn" style="border-color:#66bb6a; color:#66bb6a; flex:1;" ${looseShares === 0 ? "disabled" : ""}>Merge</button>
<button id="merge-cancel" class="alfa-main-btn" style="border-color:#888; color:#888;">Cancel</button>
</div>
</div>`;
createModal("\u2795 Merge into Position", html);
$("#merge-use-all").on("click", function() {
$("#merge-shares").val(looseShares);
updateMergePreview();
});
function updateMergePreview() {
const newPrice = parseFloat($("#merge-price").val()) || 0;
const newShares = parseInt($("#merge-shares").val()) || 0;
if (newShares <= 0) {
$("#merge-result").hide();
return;
}
const oldCost = pos.entryPrice * pos.shares;
const newCost = newPrice * newShares;
const totalShares = pos.shares + newShares;
const newAvg = totalShares > 0 ? (oldCost + newCost) / totalShares : pos.entryPrice;
$("#merge-new-avg").text(`$${newAvg.toFixed(2)}`);
$("#merge-new-shares").text(totalShares.toLocaleString());
$("#merge-result").show();
}
$("#merge-price, #merge-shares").on("input change", updateMergePreview);
if (looseShares > 0) updateMergePreview();
$("#merge-submit").on("click", function() {
const newPrice = parseFloat($("#merge-price").val()) || 0;
const newShares = parseInt($("#merge-shares").val()) || 0;
if (newShares <= 0) {
alert("Enter shares to add");
return;
}
if (newPrice <= 0) {
alert("Enter valid price");
return;
}
const oldCost = pos.entryPrice * pos.shares;
const newCost = newPrice * newShares;
const totalShares = pos.shares + newShares;
pos.entryPrice = (oldCost + newCost) / totalShares;
pos.shares = totalShares;
pos.targetPrice = pos.entryPrice * (1 + pos.targetPercent / 100);
pos.stopPrice = pos.entryPrice * (1 - pos.stopPercent / 100);
saveSwingTradeData();
closeModal();
$("#swing-positions-list").html(renderSwingPositions());
bindSwingPositionActions();
});
$("#merge-cancel").on("click", closeModal);
}
function openEditPositionModal(positionId) {
const pos = swingTradeData.activePositions.find((p) => p.id === positionId);
if (!pos) return;
const html = `
<div class="add-position-form">
<div class="form-group">
<label>Stock: ${pos.symbol}</label>
<input type="hidden" id="edit-pos-id" value="${pos.id}">
</div>
<div class="form-row">
<div class="form-group">
<label>Entry Price ($)</label>
<input type="number" id="edit-entry-price" class="alfa-input" step="0.01" value="${pos.entryPrice.toFixed(2)}">
</div>
<div class="form-group">
<label>Shares</label>
<input type="number" id="edit-shares" class="alfa-input" value="${pos.shares}">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Target</label>
<div class="form-row" style="gap:8px; align-items:center;">
<input type="number" id="edit-target" class="alfa-input" step="0.01" value="${(pos.targetPercent != null ? pos.targetPercent : pos.entryPrice && pos.targetPrice ? (pos.targetPrice / pos.entryPrice - 1) * 100 : 3).toFixed(2)}" style="flex:1;">
<select id="edit-target-unit" class="alfa-select" style="width:56px;">
<option value="pct">%</option>
<option value="dol">$</option>
</select>
</div>
<span class="form-hint" id="edit-target-hint">Target: $${(pos.targetPrice != null ? pos.targetPrice : pos.entryPrice * (1 + (pos.targetPercent || 0) / 100)).toFixed(2)}</span>
</div>
<div class="form-group">
<label>Stop</label>
<div class="form-row" style="gap:8px; align-items:center;">
<input type="number" id="edit-stop" class="alfa-input" step="0.01" value="${(pos.stopPercent != null ? pos.stopPercent : pos.entryPrice && pos.stopPrice ? (1 - pos.stopPrice / pos.entryPrice) * 100 : 2).toFixed(2)}" style="flex:1;">
<select id="edit-stop-unit" class="alfa-select" style="width:56px;">
<option value="pct">%</option>
<option value="dol">$</option>
</select>
</div>
<span class="form-hint" id="edit-stop-hint">Stop: $${(pos.stopPrice != null ? pos.stopPrice : pos.entryPrice * (1 - (pos.stopPercent || 0) / 100)).toFixed(2)}</span>
</div>
</div>
<div class="form-group">
<label>Notes</label>
<textarea id="edit-notes" class="alfa-input journal-notes-textarea" rows="4" placeholder="Trade notes...">${(pos.notes || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}</textarea>
</div>
<div class="form-actions">
<button id="edit-save" class="alfa-main-btn" style="border-color:#caa14a; color:#caa14a; flex:1;">Save Changes</button>
<button id="edit-cancel" class="alfa-main-btn" style="border-color:#888; color:#888;">Cancel</button>
</div>
</div>`;
createModal("\u270F\uFE0F Edit Position", html);
function editGetTargetStopPrices() {
const entry = parseFloat($("#edit-entry-price").val()) || pos.entryPrice;
const targetVal = parseFloat($("#edit-target").val());
const stopVal = parseFloat($("#edit-stop").val());
const targetUnit = $("#edit-target-unit").val();
const stopUnit = $("#edit-stop-unit").val();
let targetPrice, stopPrice, targetPercent, stopPercent;
if (targetUnit === "dol" && targetVal != null && !isNaN(targetVal)) {
targetPrice = targetVal;
targetPercent = entry > 0 ? (targetPrice / entry - 1) * 100 : pos.targetPercent;
} else {
targetPercent = targetVal != null && !isNaN(targetVal) ? targetVal : pos.targetPercent;
targetPrice = entry * (1 + targetPercent / 100);
}
if (stopUnit === "dol" && stopVal != null && !isNaN(stopVal)) {
stopPrice = stopVal;
stopPercent = entry > 0 ? (1 - stopPrice / entry) * 100 : pos.stopPercent;
} else {
stopPercent = stopVal != null && !isNaN(stopVal) ? stopVal : pos.stopPercent;
stopPrice = entry * (1 - stopPercent / 100);
}
return { targetPrice, stopPrice, targetPercent, stopPercent };
}
function editUpdateHints() {
const { targetPrice, stopPrice, targetPercent, stopPercent } = editGetTargetStopPrices();
$("#edit-target-hint").text($("#edit-target-unit").val() === "pct" ? `Target: $${targetPrice.toFixed(2)}` : `Target: ${targetPercent.toFixed(2)}%`);
$("#edit-stop-hint").text($("#edit-stop-unit").val() === "pct" ? `Stop: $${stopPrice.toFixed(2)}` : `Stop: ${stopPercent.toFixed(2)}%`);
}
$("#edit-entry-price, #edit-target, #edit-stop, #edit-target-unit, #edit-stop-unit").on("input change", editUpdateHints);
$("#edit-target-unit").on("change", function() {
const entry = parseFloat($("#edit-entry-price").val()) || pos.entryPrice;
const targetVal = parseFloat($("#edit-target").val());
if (entry <= 0) return;
if ($(this).val() === "dol") {
const pct = targetVal != null && !isNaN(targetVal) ? targetVal : pos.targetPercent;
$("#edit-target").val((entry * (1 + pct / 100)).toFixed(2));
} else {
const dol = targetVal != null && !isNaN(targetVal) ? targetVal : pos.targetPrice;
$("#edit-target").val(entry > 0 ? ((dol / entry - 1) * 100).toFixed(2) : pos.targetPercent);
}
editUpdateHints();
});
$("#edit-stop-unit").on("change", function() {
const entry = parseFloat($("#edit-entry-price").val()) || pos.entryPrice;
const stopVal = parseFloat($("#edit-stop").val());
if (entry <= 0) return;
if ($(this).val() === "dol") {
const pct = stopVal != null && !isNaN(stopVal) ? stopVal : pos.stopPercent;
$("#edit-stop").val((entry * (1 - pct / 100)).toFixed(2));
} else {
const dol = stopVal != null && !isNaN(stopVal) ? stopVal : pos.stopPrice;
$("#edit-stop").val(entry > 0 ? ((1 - dol / entry) * 100).toFixed(2) : pos.stopPercent);
}
editUpdateHints();
});
$("#edit-save").on("click", function() {
const id = $("#edit-pos-id").val();
const position = swingTradeData.activePositions.find((p) => p.id === id);
if (!position) return;
position.entryPrice = parseFloat($("#edit-entry-price").val()) || position.entryPrice;
position.shares = parseInt($("#edit-shares").val()) || position.shares;
const { targetPrice, stopPrice, targetPercent, stopPercent } = editGetTargetStopPrices();
position.targetPrice = targetPrice;
position.stopPrice = stopPrice;
position.targetPercent = targetPercent;
position.stopPercent = stopPercent;
position.notes = $("#edit-notes").val().trim();
saveSwingTradeData();
closeModal();
$("#swing-positions-list").html(renderSwingPositions());
bindSwingPositionActions();
});
$("#edit-cancel").on("click", closeModal);
}
function savePortfolioData() {
try {
localStorage.setItem("alfa_portfolio_data", JSON.stringify(portfolioData));
} catch (e) {
console.error("Failed to save portfolio data:", e);
}
}
async function fetchStockTransactionLogs(fullSync = false, statusCallback = null) {
const key = localStorage.getItem("alfa_vault_apikey");
if (!key) {
console.error("No API key for portfolio sync");
return { success: false, error: "No API key" };
}
const results = { buys: [], sells: [], newTransactions: 0, pagesScanned: 0, totalLogsScanned: 0 };
let hasMore = true;
let nextUrl = null;
let pagesWithNoStocks = 0;
const maxPagesWithNoStocks = 50;
const fromTimestamp = fullSync ? 0 : portfolioData.lastSyncTimestamp;
try {
let baseUrl = `https://api.torn.com/v2/user/log?key=${key}&limit=100&sort=desc&log=5510,5511`;
if (fromTimestamp > 0) {
baseUrl += `&from=${fromTimestamp}`;
}
let useTypeFilter = true;
let response = await fetch(baseUrl);
let data = await response.json();
if (data.error || !data.log) {
useTypeFilter = false;
baseUrl = `https://api.torn.com/v2/user/log?key=${key}&limit=100&sort=desc`;
if (fromTimestamp > 0) {
baseUrl += `&from=${fromTimestamp}`;
}
if (statusCallback) statusCallback("Using unfiltered log fetch (slower)...");
}
while (hasMore) {
results.pagesScanned++;
if (statusCallback) {
const stockCount = results.buys.length + results.sells.length;
statusCallback(`Scanning logs... (${stockCount} stock transactions found, page ${results.pagesScanned})`);
}
if (results.pagesScanned > 1 || !useTypeFilter) {
const url = nextUrl || baseUrl;
response = await fetch(url);
data = await response.json();
}
if (data.error) {
console.error("API Error:", data.error);
if (useTypeFilter && results.pagesScanned === 1) {
useTypeFilter = false;
baseUrl = `https://api.torn.com/v2/user/log?key=${key}&limit=100&sort=desc`;
if (fromTimestamp > 0) {
baseUrl += `&from=${fromTimestamp}`;
}
continue;
}
return { success: false, error: data.error.error || "API Error" };
}
if (!data.log || data.log.length === 0) {
hasMore = false;
break;
}
results.totalLogsScanned += data.log.length;
let foundStocksThisPage = 0;
for (const entry of data.log) {
if (entry.details && (entry.details.id === LOG_TYPE_BUY || entry.details.id === LOG_TYPE_SELL)) {
const symbol = STOCK_ID_MAP[entry.data.stock];
if (!symbol) continue;
const transaction = {
id: entry.id,
timestamp: entry.timestamp,
type: entry.details.id === LOG_TYPE_BUY ? "buy" : "sell",
symbol,
stockId: entry.data.stock,
shares: entry.data.amount,
totalValue: entry.data.worth,
pricePerShare: parseFloat(String(entry.data.price).replace(/,/g, "")),
fees: entry.data.fees || 0,
profit: entry.data.profit || 0
// Only present on sells
};
if (transaction.type === "buy") {
results.buys.push(transaction);
} else {
results.sells.push(transaction);
}
results.newTransactions++;
foundStocksThisPage++;
}
}
if (!useTypeFilter) {
if (foundStocksThisPage === 0) {
pagesWithNoStocks++;
if (pagesWithNoStocks >= maxPagesWithNoStocks && results.newTransactions > 0) {
if (statusCallback) {
statusCallback(`Reached end of stock history (${maxPagesWithNoStocks} pages with no stocks)`);
}
hasMore = false;
break;
}
} else {
pagesWithNoStocks = 0;
}
}
if (data._metadata && data._metadata.links && data._metadata.links.prev) {
const prevLink = data._metadata.links.prev;
if (prevLink.includes("?")) {
nextUrl = prevLink + `&key=${key}`;
} else {
nextUrl = prevLink + `?key=${key}`;
}
} else {
hasMore = false;
}
if (hasMore) {
await new Promise((r) => setTimeout(r, 150));
}
}
if (statusCallback) {
statusCallback(`Found ${results.newTransactions} stock transactions in ${results.pagesScanned} pages`);
}
return { success: true, ...results };
} catch (e) {
console.error("Transaction fetch error:", e);
return { success: false, error: e.message };
}
}
function processTransactions(fetchResult) {
if (!fetchResult.success) return;
const allTransactions = [...fetchResult.buys, ...fetchResult.sells];
allTransactions.sort((a, b) => a.timestamp - b.timestamp);
const existingIds = new Set(portfolioData.transactions.map((t) => t.id));
const newTransactions = allTransactions.filter((t) => !existingIds.has(t.id));
if (newTransactions.length === 0) {
return { added: 0 };
}
portfolioData.transactions.push(...newTransactions);
portfolioData.transactions.sort((a, b) => a.timestamp - b.timestamp);
if (portfolioData.transactions.length > 0) {
const latest = portfolioData.transactions[portfolioData.transactions.length - 1];
portfolioData.lastSyncTimestamp = latest.timestamp;
}
recalculateCostBasis();
savePortfolioData();
return { added: newTransactions.length };
}
function getLooseSharesAvgPrice(symbol, looseShares) {
const basis = portfolioData.costBasis[symbol];
if (!basis?.lots?.length || looseShares <= 0) return null;
let sharesNeeded = looseShares;
let totalCost = 0;
let sharesTaken = 0;
for (let i = basis.lots.length - 1; i >= 0 && sharesNeeded > 0; i--) {
const lot = basis.lots[i];
const take = Math.min(lot.shares, sharesNeeded);
totalCost += take * lot.pricePerShare;
sharesTaken += take;
sharesNeeded -= take;
}
return sharesTaken > 0 ? totalCost / sharesTaken : null;
}
function recalculateCostBasis() {
portfolioData.costBasis = {};
portfolioData.realizedPL = 0;
for (const tx of portfolioData.transactions) {
const sym = tx.symbol;
if (!portfolioData.costBasis[sym]) {
portfolioData.costBasis[sym] = {
totalShares: 0,
totalCost: 0,
lots: [],
// FIFO lots: [{ shares, pricePerShare, timestamp }]
realizedPL: 0,
totalBought: 0,
totalSold: 0
};
}
const basis = portfolioData.costBasis[sym];
if (tx.type === "buy") {
basis.lots.push({
shares: tx.shares,
pricePerShare: tx.pricePerShare,
timestamp: tx.timestamp
});
basis.totalShares += tx.shares;
basis.totalCost += tx.totalValue;
basis.totalBought += tx.totalValue;
} else {
let sharesToSell = tx.shares;
let costOfSold = 0;
while (sharesToSell > 0 && basis.lots.length > 0) {
const lot = basis.lots[0];
if (lot.shares <= sharesToSell) {
costOfSold += lot.shares * lot.pricePerShare;
sharesToSell -= lot.shares;
basis.lots.shift();
} else {
costOfSold += sharesToSell * lot.pricePerShare;
lot.shares -= sharesToSell;
sharesToSell = 0;
}
}
basis.totalShares -= tx.shares;
basis.totalCost -= costOfSold;
basis.totalSold += tx.totalValue;
basis.realizedPL += tx.profit;
portfolioData.realizedPL += tx.profit;
}
}
}
function getPeriodStartTimestamp(period) {
const now = Math.floor(Date.now() / 1e3);
const today = /* @__PURE__ */ new Date();
switch (period) {
case "day":
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
return Math.floor(startOfDay.getTime() / 1e3);
case "week":
return now - 7 * 24 * 60 * 60;
case "month":
return now - 30 * 24 * 60 * 60;
case "year":
return now - 365 * 24 * 60 * 60;
case "ytd":
const startOfYear = new Date(today.getFullYear(), 0, 1);
return Math.floor(startOfYear.getTime() / 1e3);
case "all":
default:
return 0;
}
}
function calculatePeriodPL(period = "all") {
const startTimestamp = getPeriodStartTimestamp(period);
const periodTransactions = portfolioData.transactions.filter((tx) => tx.timestamp >= startTimestamp);
const result = {
period,
startTimestamp,
buys: periodTransactions.filter((tx) => tx.type === "buy"),
sells: periodTransactions.filter((tx) => tx.type === "sell"),
totalBuyValue: 0,
totalSellValue: 0,
realizedPL: 0,
fees: 0,
netInvested: 0
// Buys - Sells (net cash flow into stocks)
};
for (const tx of result.buys) {
result.totalBuyValue += tx.totalValue;
}
for (const tx of result.sells) {
result.totalSellValue += tx.totalValue;
result.realizedPL += tx.profit || 0;
result.fees += tx.fees || 0;
}
result.netInvested = result.totalBuyValue - result.totalSellValue;
return result;
}
function getPeriodLabel(period) {
switch (period) {
case "day":
return "Today";
case "week":
return "7 Days";
case "month":
return "30 Days";
case "year":
return "1 Year";
case "ytd":
return "YTD";
case "all":
return "All Time";
default:
return period;
}
}
function calculatePortfolioPL() {
const result = {
totalCostBasis: 0,
totalCurrentValue: 0,
unrealizedPL: 0,
unrealizedPLPercent: 0,
realizedPL: portfolioData.realizedPL,
totalFees: 0,
perStock: {},
hasIncompleteData: false
// Flag when we have sells without matching buys
};
const allSymbols = /* @__PURE__ */ new Set([
...Object.keys(portfolioData.costBasis),
...Object.keys(STOCK_DATA)
]);
for (const sym of allSymbols) {
const basis = portfolioData.costBasis[sym] || {
totalShares: 0,
totalCost: 0,
realizedPL: 0,
totalBought: 0,
totalSold: 0,
lots: []
};
const actualShares = getOwnedShares(sym);
const currentPrice = getPrice(sym);
const currentValue = actualShares * currentPrice;
if (actualShares <= 0 && basis.totalBought === 0 && basis.realizedPL === 0) continue;
const trackedShares = Math.max(0, basis.totalShares);
let avgCostPerShare = 0;
let costBasisForHoldings = 0;
if (actualShares > 0 && basis.lots && basis.lots.length > 0) {
let sharesAccounted = 0;
for (const lot of basis.lots) {
if (sharesAccounted >= actualShares) break;
const lotShares = Math.min(lot.shares, actualShares - sharesAccounted);
costBasisForHoldings += lotShares * lot.pricePerShare;
sharesAccounted += lotShares;
}
if (actualShares > sharesAccounted) {
costBasisForHoldings += (actualShares - sharesAccounted) * currentPrice;
}
avgCostPerShare = actualShares > 0 ? costBasisForHoldings / actualShares : 0;
} else if (trackedShares > 0 && basis.totalCost > 0) {
avgCostPerShare = basis.totalCost / trackedShares;
if (actualShares <= trackedShares) {
costBasisForHoldings = actualShares * avgCostPerShare;
} else {
costBasisForHoldings = basis.totalCost + (actualShares - trackedShares) * currentPrice;
}
}
if (basis.totalShares < 0 || actualShares > 0 && trackedShares === 0 && basis.totalBought === 0) {
result.hasIncompleteData = true;
}
const unrealizedPL = currentValue - costBasisForHoldings;
const unrealizedPLPercent = costBasisForHoldings > 0 ? unrealizedPL / costBasisForHoldings * 100 : 0;
result.perStock[sym] = {
shares: actualShares,
trackedShares,
avgCost: avgCostPerShare,
totalCost: costBasisForHoldings,
currentPrice,
currentValue,
unrealizedPL,
unrealizedPLPercent,
realizedPL: basis.realizedPL,
totalBought: basis.totalBought,
totalSold: basis.totalSold,
hasIncompleteData: basis.totalShares < 0 || actualShares > 0 && basis.totalBought === 0
};
if (actualShares > 0) {
result.totalCostBasis += costBasisForHoldings;
result.totalCurrentValue += currentValue;
}
}
result.unrealizedPL = result.totalCurrentValue - result.totalCostBasis;
result.unrealizedPLPercent = result.totalCostBasis > 0 ? result.unrealizedPL / result.totalCostBasis * 100 : 0;
for (const tx of portfolioData.transactions) {
if (tx.type === "sell") {
result.totalFees += tx.fees || 0;
}
}
return result;
}
async function fullPortfolioSync(statusCallback = null) {
if (statusCallback) statusCallback("Starting full sync...");
const result = await fetchStockTransactionLogs(true, statusCallback);
if (!result.success) {
if (statusCallback) statusCallback(`Sync failed: ${result.error}`);
return { success: false, error: result.error };
}
const allTransactions = [...result.buys || [], ...result.sells || []];
allTransactions.sort((a, b) => a.timestamp - b.timestamp);
if (allTransactions.length === 0) {
if (statusCallback) {
statusCallback("Sync returned 0 transactions \u2014 keeping existing portfolio data");
}
return { success: false, error: "No transactions returned", keptExisting: true };
}
portfolioData.transactions = allTransactions;
portfolioData.lastSyncTimestamp = allTransactions[allTransactions.length - 1].timestamp;
recalculateCostBasis();
portfolioData.lastFullSync = Date.now();
savePortfolioData();
if (statusCallback) {
statusCallback(`Synced ${allTransactions.length} transactions`);
}
return { success: true, transactions: allTransactions.length };
}
async function incrementalPortfolioSync(statusCallback = null) {
if (statusCallback) statusCallback("Checking for new transactions...");
const result = await fetchStockTransactionLogs(false, statusCallback);
if (result.success) {
const processed = processTransactions(result);
if (statusCallback) {
if (processed && processed.added > 0) {
statusCallback(`Added ${processed.added} new transactions`);
} else {
statusCallback("Portfolio up to date");
}
}
return { success: true, added: processed ? processed.added : 0 };
} else {
if (statusCallback) statusCallback(`Sync failed: ${result.error}`);
return { success: false, error: result.error };
}
}
const ADVISOR_ITEMS = {
364: "Box of Grenades",
365: "Box of Medical Supplies",
366: "Erotic DVD",
367: "Feathery Hotel Coupon",
368: "Lawyer's Business Card",
369: "Lottery Voucher",
370: "Drug Pack",
817: "Six-Pack of Alcohol",
818: "Six-Pack of Energy Drink",
1057: "Gentleman's Cache",
1112: "Elegant Cache",
1113: "Naughty Cache",
1114: "Elderly Cache",
1115: "Denim Cache",
1116: "Wannabe Cache",
1117: "Cutesy Cache"
};
const TCC_CACHE_IDS = [1057, 1112, 1113, 1114, 1115, 1116, 1117];
const ADVISOR_DATA = {
"MUN": { type: "item", id: 818, freq: 7 },
"ASS": { type: "item", id: 817, freq: 7 },
"HRG": { type: "manual", label: "Avg Property Value", freq: 31 },
"LSC": { type: "item", id: 369, freq: 7 },
"LAG": { type: "item", id: 368, freq: 7 },
"FHG": { type: "item", id: 367, freq: 7 },
"PRN": { type: "item", id: 366, freq: 7 },
"SYM": { type: "item", id: 370, freq: 7 },
"TCC": { type: "average", ids: TCC_CACHE_IDS, freq: 31 },
"THS": { type: "item", id: 365, freq: 7 },
"EWM": { type: "item", id: 364, freq: 7 },
"BAG": { type: "passive" },
"CNC": { type: "cash", val: 8e7, freq: 31 },
"TSB": { type: "cash", val: 5e7, freq: 31 },
"TMI": { type: "cash", val: 25e6, freq: 31 },
"IOU": { type: "cash", val: 12e6, freq: 31 },
"GRN": { type: "cash", val: 4e6, freq: 31 },
"TCT": { type: "cash", val: 1e6, freq: 31 }
};
let lastNwCache = null;
let lastSync = 0;
let itemPrices = {};
try {
itemPrices = JSON.parse(localStorage.getItem("alfa_advisor_prices")) || {};
} catch (e) {
itemPrices = {};
}
let networthSettings = { sources: { inventory: false, points: true, stocks: true }, excludedStocks: [], excludeMode: "all" };
try {
let savedNW = JSON.parse(localStorage.getItem("alfa_advisor_networth"));
if (savedNW) {
networthSettings = savedNW;
if (!networthSettings.excludeMode) networthSettings.excludeMode = "all";
}
} catch (e) {
}
const BANK_BASE_RATES = { "1w": 0.7917, "2w": 1.7833, "1m": 4.3, "2m": 9.8, "3m": 16.5 };
let bankSettings = { roi_1w: 0, roi_2w: 0, roi_1m: 0, roi_2m: 0, roi_3m: 0, active_period: "2w" };
try {
let savedBank = JSON.parse(localStorage.getItem("alfa_advisor_bank"));
if (savedBank) bankSettings = savedBank;
} catch (e) {
}
let stocks = {}, stockId = {}, stockRows = {}, localShareCache = {};
const TORNSY_API = "https://tornsy.com/api";
const CACHE_TTL = {
stocks: 60 * 1e3,
// 1 minute (matches Tornsy update frequency)
ohlc: 5 * 60 * 1e3,
// 5 minutes (historical data doesn't change much)
analysis: 5 * 60 * 1e3
// 5 minutes (computed indicators)
};
const DEFAULT_VAULT_CONFIG = {
stocks: [],
lockedStocks: [],
// Stocks protected from vault spread/withdraw
depositStrategy: "swing_strategy",
// swing_strategy | equal | dip_weighted | rsi_weighted | roi_priority
withdrawStrategy: "proportional",
// loose_first | proportional | worst_roi | best_performers | swing_strategy
keepAmount: 5e3,
skipOvervalued: false,
// Smart spread: skip stocks with SELL/TAKE PROFIT signals
minimalMode: false,
// Show only Vault Spread, Rebalance, Withdraw + Lock Blocks/Smart Mode
modules: { portfolio: true, gamble: true, swing: true, stocks: true },
// Visibility: Settings → Modules
excludeLockedBlocksFromValue: false
// When true, Total Vault Value = loose + swing only (excludes locked blocks)
};
const VAULT_SWING_PRESET_SYMBOLS = ["ASS", "BAG", "CBD", "CNC", "ELT", "EWM", "FHG", "GRN", "IOU", "IST", "LAG", "LSC", "MCS", "MUN", "PRN", "SYM", "SYS", "TCC", "TCI", "TCM", "TCP", "TCT", "TGP", "THS", "TMI", "TSB", "WLT"];
let vaultConfig = {};
try {
let saved = JSON.parse(localStorage.getItem("alfa_vault_config"));
if (saved) vaultConfig = { ...DEFAULT_VAULT_CONFIG, ...saved };
else vaultConfig = { ...DEFAULT_VAULT_CONFIG };
} catch (e) {
vaultConfig = { ...DEFAULT_VAULT_CONFIG };
}
let vaultAnalysisCache = {};
const DEFAULT_GAMBLE_CONFIG = {
targetStock: "",
depositPresets: [
{ label: "50k", amount: 5e4 },
{ label: "100k", amount: 1e5 },
{ label: "500k", amount: 5e5 },
{ label: "1m", amount: 1e6 },
{ label: "All", amount: -1 }
],
withdrawPresets: [
{ label: "50k", amount: 5e4 },
{ label: "100k", amount: 1e5 },
{ label: "500k", amount: 5e5 },
{ label: "1m", amount: 1e6 },
{ label: "All", amount: -1 }
],
requireConfirmation: false
};
let gambleConfig = {};
try {
let saved = JSON.parse(localStorage.getItem("alfa_gamble_config"));
if (saved) gambleConfig = { ...DEFAULT_GAMBLE_CONFIG, ...saved };
else gambleConfig = { ...DEFAULT_GAMBLE_CONFIG };
} catch (e) {
gambleConfig = { ...DEFAULT_GAMBLE_CONFIG };
}
function saveGambleConfig() {
try {
localStorage.setItem("alfa_gamble_config", JSON.stringify(gambleConfig));
} catch (e) {
console.error("Failed to save gamble config:", e);
}
}
function getGambleTargetStock() {
if (gambleConfig.targetStock && vaultConfig.stocks.includes(gambleConfig.targetStock)) {
return gambleConfig.targetStock;
}
return vaultConfig.stocks[0] || "";
}
let vaultCacheMem = null;
let vaultCacheFlushTimer = null;
function loadVaultCacheMem() {
if (vaultCacheMem) return vaultCacheMem;
try {
vaultCacheMem = JSON.parse(localStorage.getItem("alfa_vault_cache")) || {};
} catch (e) {
vaultCacheMem = {};
}
return vaultCacheMem;
}
function scheduleVaultCacheFlush() {
if (vaultCacheFlushTimer) return;
vaultCacheFlushTimer = setTimeout(() => {
vaultCacheFlushTimer = null;
try {
localStorage.setItem("alfa_vault_cache", JSON.stringify(vaultCacheMem || {}));
} catch (e) {
console.error("Cache write error:", e);
}
}, 500);
}
function getCached(key, maxAge) {
try {
const cache = loadVaultCacheMem();
const entry = cache[key];
if (entry && Date.now() - entry.timestamp < maxAge) {
return entry.data;
}
} catch (e) {
console.error("Cache read error:", e);
}
return null;
}
function setCache(key, data) {
try {
const cache = loadVaultCacheMem();
cache[key] = { data, timestamp: Date.now() };
scheduleVaultCacheFlush();
} catch (e) {
console.error("Cache write error:", e);
}
}
function clearVaultCache() {
vaultCacheMem = {};
if (vaultCacheFlushTimer) {
clearTimeout(vaultCacheFlushTimer);
vaultCacheFlushTimer = null;
}
try {
localStorage.removeItem("alfa_vault_cache");
} catch (e) {
}
}
let _gmXhrFn = void 0;
let _tornsyUnavailableWarned = false;
function resolveGmXhr() {
if (_gmXhrFn !== void 0) return _gmXhrFn;
try {
if (typeof GM_xmlhttpRequest === "function") {
_gmXhrFn = GM_xmlhttpRequest;
return _gmXhrFn;
}
} catch (e) {
}
try {
if (typeof GM !== "undefined" && GM && typeof GM.xmlHttpRequest === "function") {
_gmXhrFn = GM.xmlHttpRequest.bind(GM);
return _gmXhrFn;
}
} catch (e) {
}
try {
if (typeof unsafeWindow !== "undefined" && unsafeWindow && typeof unsafeWindow.GM_xmlhttpRequest === "function") {
_gmXhrFn = unsafeWindow.GM_xmlhttpRequest;
return _gmXhrFn;
}
} catch (e) {
}
_gmXhrFn = null;
return null;
}
function isGmXhrAvailable() {
return !!resolveGmXhr();
}
function warnTornsyUnavailable(extra) {
if (_tornsyUnavailableWarned) return;
_tornsyUnavailableWarned = true;
const msg = extra || "Tornsy unavailable (no GM_xmlhttpRequest). Using DOM prices; signal weighting skipped. Desktop Tampermonkey/Violentmonkey recommended.";
console.warn("[Smart Stock Vault]", msg);
try {
const el = $("#vault-status");
if (el && el.length) {
el.html('<span style="color:#ffb74d;">' + msg + "</span>");
}
} catch (e) {
}
}
function gmFetch(url) {
return new Promise((resolve) => {
const xhr = resolveGmXhr();
if (!xhr) {
warnTornsyUnavailable();
resolve(null);
return;
}
try {
xhr({
method: "GET",
url,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
resolve(data);
} catch (e) {
console.error("JSON parse error:", e);
resolve(null);
}
},
onerror: function(error) {
console.error("GM_xmlhttpRequest error:", error);
resolve(null);
}
});
} catch (e) {
console.error("GM_xmlhttpRequest invoke error:", e);
warnTornsyUnavailable("Tornsy request failed to start: " + (e && e.message ? e.message : e));
resolve(null);
}
});
}
async function fetchWithCache(url, cacheKey, maxAge) {
const cached = getCached(cacheKey, maxAge);
if (cached) return cached;
try {
let data;
if (url.includes("tornsy.com")) {
if (!isGmXhrAvailable()) {
warnTornsyUnavailable();
return null;
}
data = await gmFetch(url);
} else {
const res = await fetch(url);
data = await res.json();
}
if (data) setCache(cacheKey, data);
return data;
} catch (e) {
console.error("Fetch error:", e);
return null;
}
}
async function fetchTornsyStocks() {
const url = `${TORNSY_API}/stocks?interval=d1,d7,d14,d30`;
return await fetchWithCache(url, "tornsy_stocks", CACHE_TTL.stocks);
}
async function fetchTornsyOHLC(symbol, interval = "d1", limit = 30) {
const url = `${TORNSY_API}/${symbol.toLowerCase()}?interval=${interval}&limit=${limit}`;
return await fetchWithCache(url, `tornsy_ohlc_${symbol}_${interval}`, CACHE_TTL.ohlc);
}
function calculateSMA(ohlcData, period) {
if (!ohlcData || ohlcData.length < period) return 0;
const closes = ohlcData.slice(-period).map((d) => parseFloat(d[4] || d[1]));
return closes.reduce((a, b) => a + b, 0) / period;
}
function calculateEMA(ohlcData, period) {
if (!ohlcData || ohlcData.length < period) return 0;
const closes = ohlcData.map((d) => parseFloat(d[4] || d[1]));
const multiplier = 2 / (period + 1);
let ema = closes.slice(0, period).reduce((a, b) => a + b, 0) / period;
for (let i = period; i < closes.length; i++) {
ema = (closes[i] - ema) * multiplier + ema;
}
return ema;
}
function calculateRSI(ohlcData, period = 14) {
if (!ohlcData || ohlcData.length < period + 1) return 50;
const closes = ohlcData.map((d) => parseFloat(d[4] || d[1]));
let gains = 0, losses = 0;
for (let i = 1; i <= period; i++) {
const change = closes[i] - closes[i - 1];
if (change > 0) gains += change;
else losses += Math.abs(change);
}
let avgGain = gains / period;
let avgLoss = losses / period;
for (let i = period + 1; i < closes.length; i++) {
const change = closes[i] - closes[i - 1];
if (change > 0) {
avgGain = (avgGain * (period - 1) + change) / period;
avgLoss = avgLoss * (period - 1) / period;
} else {
avgGain = avgGain * (period - 1) / period;
avgLoss = (avgLoss * (period - 1) + Math.abs(change)) / period;
}
}
if (avgLoss === 0) return 100;
const rs = avgGain / avgLoss;
return 100 - 100 / (1 + rs);
}
function calculateVolatility(ohlcData, period = 14) {
if (!ohlcData || ohlcData.length < period + 1) return 0;
const closes = ohlcData.slice(-period - 1).map((d) => parseFloat(d[4] || d[1]));
const returns = [];
for (let i = 1; i < closes.length; i++) {
const ret = (closes[i] - closes[i - 1]) / closes[i - 1];
returns.push(ret);
}
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
const variance = returns.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / returns.length;
return Math.sqrt(variance) * 100;
}
function calculateBollingerBands(ohlcData, period = 20, stdDev = 2) {
if (!ohlcData || ohlcData.length < period) return { upper: 0, middle: 0, lower: 0, position: 0.5 };
const closes = ohlcData.slice(-period).map((d) => parseFloat(d[4] || d[1]));
const middle = closes.reduce((a, b) => a + b, 0) / period;
const squaredDiffs = closes.map((c) => Math.pow(c - middle, 2));
const variance = squaredDiffs.reduce((a, b) => a + b, 0) / period;
const sd = Math.sqrt(variance);
const upper = middle + stdDev * sd;
const lower = middle - stdDev * sd;
const currentPrice = closes[closes.length - 1];
const position = upper - lower > 0 ? (currentPrice - lower) / (upper - lower) : 0.5;
return { upper, middle, lower, position: Math.max(0, Math.min(1, position)) };
}
function calculateMomentum(ohlcData, period = 7) {
if (!ohlcData || ohlcData.length < period + 1) return 0;
const closes = ohlcData.map((d) => parseFloat(d[4] || d[1]));
const currentPrice = closes[closes.length - 1];
const pastPrice = closes[closes.length - 1 - period];
if (pastPrice === 0) return 0;
return (currentPrice - pastPrice) / pastPrice * 100;
}
function calculateADXAndATR(ohlcData, period = 14) {
const defaultResult = { adx: 25, atr: 0 };
if (!ohlcData || ohlcData.length < period + 2) return defaultResult;
const highs = ohlcData.map((d) => parseFloat(d[2] || d[1]));
const lows = ohlcData.map((d) => parseFloat(d[3] || d[1]));
const closes = ohlcData.map((d) => parseFloat(d[4] || d[1]));
const tr = [];
const plusDM = [];
const minusDM = [];
for (let i = 1; i < ohlcData.length; i++) {
const high = highs[i], low = lows[i], prevHigh = highs[i - 1], prevLow = lows[i - 1], prevClose = closes[i - 1];
tr.push(Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose)));
const upMove = high - prevHigh;
const downMove = prevLow - low;
plusDM.push(upMove > downMove && upMove > 0 ? upMove : 0);
minusDM.push(downMove > upMove && downMove > 0 ? downMove : 0);
}
function wilderSmooth(arr) {
const result = [];
let sum = 0;
for (let i = 0; i < arr.length; i++) {
if (i < period) {
sum += arr[i];
result.push(i === period - 1 ? sum / period : 0);
} else {
const smoothed = (result[i - 1] * (period - 1) + arr[i]) / period;
result.push(smoothed);
}
}
return result;
}
const smoothedTR = wilderSmooth(tr);
const smoothedPlusDM = wilderSmooth(plusDM);
const smoothedMinusDM = wilderSmooth(minusDM);
const dxValues = [];
for (let i = period; i < smoothedTR.length; i++) {
const atr2 = smoothedTR[i];
if (atr2 <= 0) {
dxValues.push(0);
continue;
}
const plusDI = 100 * (smoothedPlusDM[i] / atr2);
const minusDI = 100 * (smoothedMinusDM[i] / atr2);
const diSum = plusDI + minusDI;
const dx = diSum > 0 ? 100 * Math.abs(plusDI - minusDI) / diSum : 0;
dxValues.push(dx);
}
if (dxValues.length < period) return defaultResult;
let adxSum = dxValues.slice(0, period).reduce((a, b) => a + b, 0) / period;
for (let i = period; i < dxValues.length; i++) {
adxSum = (adxSum * (period - 1) + dxValues[i]) / period;
}
const atr = smoothedTR[smoothedTR.length - 1] || 0;
return {
adx: Math.min(100, Math.max(0, adxSum)),
atr
};
}
function calculateADX(ohlcData, period = 14) {
const result = calculateADXAndATR(ohlcData, period);
return typeof result === "object" ? result.adx : result;
}
function calculateVolumeMultiplier(ohlcData, period = 7) {
if (!ohlcData || ohlcData.length < period + 1) return 1;
const volumes = ohlcData.map((d) => parseFloat(d[5] || 0));
const currentVolume = volumes[volumes.length - 1];
const prevVolumes = volumes.slice(-period - 1, -1);
const avgVolume = prevVolumes.reduce((a, b) => a + b, 0) / period;
if (avgVolume <= 0 || currentVolume <= 0) return 1;
return currentVolume / avgVolume;
}
function findSupportResistance(ohlcData) {
if (!ohlcData || ohlcData.length === 0) return { support: 0, resistance: 0 };
let high = -Infinity, low = Infinity;
for (const candle of ohlcData) {
const candleHigh = parseFloat(candle[2] || candle[1]);
const candleLow = parseFloat(candle[3] || candle[1]);
if (candleHigh > high) high = candleHigh;
if (candleLow < low) low = candleLow;
}
return { support: low, resistance: high };
}
function getSignalFromAnalysis(analysis) {
let signal = "hold";
if (analysis.rsi > 70 && analysis.dipFrom7d > 5) signal = "sell";
else if (analysis.rsi > 60 || analysis.dipFrom7d > 3) signal = "take_profit";
else if (analysis.rsi < 30 && analysis.dipFrom7d < -3) signal = "buy";
else if (analysis.rsi < 40 || analysis.dipFrom7d < -2) signal = "good";
const adx = analysis.adx ?? 25;
const adxMin = swingTradeData?.settings?.adxMinThreshold ?? 25;
const adxPartial = swingTradeData?.settings?.adxPartialThreshold ?? 20;
if (adx < adxPartial) {
if (signal === "sell") signal = "take_profit";
else if (signal === "take_profit" || signal === "buy" || signal === "good") signal = "hold";
} else if (adx < adxMin) {
if (signal === "sell") signal = "take_profit";
else if (signal === "take_profit") signal = "hold";
else if (signal === "buy") signal = "good";
}
const volMult = analysis.volumeMultiplier ?? 1;
const volMin = swingTradeData?.settings?.volumeMinMultiplier ?? 1;
const volPartial = swingTradeData?.settings?.volumePartialMultiplier ?? 0.8;
if (volMult < volPartial) {
if (signal === "sell") signal = "take_profit";
else if (signal === "take_profit" || signal === "buy" || signal === "good") signal = "hold";
} else if (volMult < volMin) {
if (signal === "take_profit") signal = "hold";
else if (signal === "buy") signal = "good";
}
return signal;
}
async function analyzeStockForVault(symbol) {
const cached = getCached(`analysis_${symbol}`, CACHE_TTL.analysis);
if (cached) {
vaultAnalysisCache[symbol] = cached;
return cached;
}
if (!isGmXhrAvailable()) {
warnTornsyUnavailable();
return null;
}
try {
const ohlcResult = await fetchTornsyOHLC(symbol, "d1", 30);
if (!ohlcResult || !ohlcResult.data || ohlcResult.data.length === 0) {
return null;
}
const ohlcData = ohlcResult.data;
const currentPrice = parseFloat(ohlcData[ohlcData.length - 1][4] || ohlcData[ohlcData.length - 1][1]);
const sma7 = calculateSMA(ohlcData, 7);
const sma14 = calculateSMA(ohlcData, 14);
const sma30 = calculateSMA(ohlcData, Math.min(30, ohlcData.length));
const rsi = calculateRSI(ohlcData, 14);
const volatility = calculateVolatility(ohlcData, 14);
const bollinger = calculateBollingerBands(ohlcData, 20, 2);
const momentum7d = calculateMomentum(ohlcData, 7);
const { support, resistance } = findSupportResistance(ohlcData);
const { adx, atr } = calculateADXAndATR(ohlcData, 14);
const volumeMultiplier = calculateVolumeMultiplier(ohlcData, 7);
const analysis = {
symbol,
price: currentPrice,
sma7,
sma14,
sma30,
dipFrom7d: sma7 > 0 ? (currentPrice - sma7) / sma7 * 100 : 0,
dipFrom14d: sma14 > 0 ? (currentPrice - sma14) / sma14 * 100 : 0,
dipFrom30d: sma30 > 0 ? (currentPrice - sma30) / sma30 * 100 : 0,
rsi,
volatility,
bollingerPosition: bollinger.position,
bollingerUpper: bollinger.upper,
bollingerLower: bollinger.lower,
momentum7d,
support,
resistance,
adx,
atr,
volumeMultiplier,
signal: "neutral",
timestamp: Date.now()
};
analysis.signal = getSignalFromAnalysis(analysis);
setCache(`analysis_${symbol}`, analysis);
vaultAnalysisCache[symbol] = analysis;
return analysis;
} catch (e) {
console.error(`Analysis error for ${symbol}:`, e);
return null;
}
}
async function analyzeAllVaultStocks(statusCallback) {
const results = {};
const stocksToAnalyze = vaultConfig.stocks.length > 0 ? vaultConfig.stocks : Object.keys(STOCK_DATA);
for (let i = 0; i < stocksToAnalyze.length; i++) {
const sym = stocksToAnalyze[i];
if (statusCallback) statusCallback(`Analyzing ${sym}... (${i + 1}/${stocksToAnalyze.length})`);
const analysis = await analyzeStockForVault(sym);
if (analysis) {
results[sym] = analysis;
}
if (i < stocksToAnalyze.length - 1) {
await new Promise((r) => setTimeout(r, 100));
}
}
vaultAnalysisCache = results;
return results;
}
async function getQuickStockAnalysis() {
const stocksData = await fetchTornsyStocks();
if (!stocksData || !stocksData.data) return {};
const results = {};
const intervals = stocksData.intervals || {};
for (const stock of stocksData.data) {
if (stock.index) continue;
const sym = stock.stock;
const currentPrice = parseFloat(stock.price);
let price7d = currentPrice, price14d = currentPrice, price30d = currentPrice;
if (stock.interval) {
if (stock.interval.d7) price7d = parseFloat(stock.interval.d7.price);
if (stock.interval.d14) price14d = parseFloat(stock.interval.d14.price);
if (stock.interval.d30) price30d = parseFloat(stock.interval.d30.price);
}
results[sym] = {
symbol: sym,
price: currentPrice,
dipFrom7d: price7d > 0 ? (currentPrice - price7d) / price7d * 100 : 0,
dipFrom14d: price14d > 0 ? (currentPrice - price14d) / price14d * 100 : 0,
dipFrom30d: price30d > 0 ? (currentPrice - price30d) / price30d * 100 : 0,
investors: stock.investors || 0
};
}
return results;
}
function loadVaultConfig() {
try {
const saved = JSON.parse(localStorage.getItem("alfa_vault_config"));
if (saved) vaultConfig = { ...DEFAULT_VAULT_CONFIG, ...saved };
else vaultConfig = { ...DEFAULT_VAULT_CONFIG };
} catch (e) {
vaultConfig = { ...DEFAULT_VAULT_CONFIG };
}
return vaultConfig;
}
function saveVaultConfig() {
try {
localStorage.setItem("alfa_vault_config", JSON.stringify(vaultConfig));
} catch (e) {
console.error("Failed to save vault config:", e);
}
}
function openSettingsModal() {
const savedKey = localStorage.getItem("alfa_vault_apikey") || "";
const lockBlocksChecked = localStorage.getItem("alfa_vault_lock") === "true";
const allStocks = Object.keys(STOCK_DATA).sort();
const stockCheckboxes = allStocks.map((sym) => {
const isSelected = vaultConfig.stocks.includes(sym);
const stockData = STOCK_DATA[sym];
const typeLabel = stockData.type === "P" ? "P" : "A";
return `<label class="vault-stock-option ${isSelected ? "selected" : ""}" data-sym="${sym}">
<input type="checkbox" class="vault-stock-check" value="${sym}" ${isSelected ? "checked" : ""}>
<span class="vault-stock-sym">${sym}</span>
<span class="vault-stock-type">${typeLabel}</span>
</label>`;
}).join("");
const bankInputs = ["1w", "2w", "1m", "2m", "3m"].map(
(t) => `<div style="display:flex; justify-content:space-between; align-items:center;">
<span style="font-size:11px; color:#aaa; font-weight:bold; width:35px;">${t}</span>
<input id="bank-${t}" class="alfa-tbl-input" style="width:70px;" value="${bankSettings["roi_" + t] || 0}">
</div>`
).join("");
const mod = vaultConfig.modules || { ...DEFAULT_VAULT_CONFIG.modules };
const html = `
<div class="settings-tabs">
<button class="settings-tab active" data-tab="api">API</button>
<button class="settings-tab" data-tab="vault">Vault</button>
<button class="settings-tab" data-tab="protection">Protection</button>
<button class="settings-tab" data-tab="modules">Modules</button>
<button class="settings-tab" data-tab="advanced">Advanced</button>
</div>
<div class="settings-content">
<!-- API Tab -->
<div class="settings-tab-content" id="settings-tab-api">
<div class="settings-section">
<div class="settings-section-title">Torn API Key</div>
<input type="password" id="settings-apikey" class="alfa-input" style="width:100%; text-align:center; letter-spacing:2px; margin-bottom:10px;" placeholder="Paste your API key here" value="${savedKey}">
<div id="settings-api-status" class="settings-api-status">
${savedKey ? '<span style="color:#8bc34a;">\u2713 API key configured</span>' : '<span style="color:#888;">No API key set</span>'}
</div>
<div class="settings-warning">
<strong>Note:</strong> Full Access API key required for P&L tracking and transaction history features.
</div>
<button id="settings-validate-key" class="alfa-mini-btn" style="width:100%; margin-top:10px; border-color:#caa14a; color:#caa14a;">Validate API Key</button>
</div>
</div>
<!-- Vault Tab -->
<div class="settings-tab-content" id="settings-tab-vault" style="display:none;">
<div class="settings-section">
<div class="settings-section-title">
Vault Stocks
<span class="settings-stock-count">Selected: <strong id="vault-selected-count">${vaultConfig.stocks.length}</strong></span>
</div>
<div class="vault-stock-grid" id="vault-stock-grid">
${stockCheckboxes}
</div>
<div class="vault-quick-actions" style="margin-top:10px;">
<button id="vault-select-none" class="alfa-mini-btn" style="border-color:#888; color:#888;">Clear All</button>
<button id="vault-select-active" class="alfa-mini-btn" style="border-color:#caa14a; color:#caa14a;">Select Active (A)</button>
<button id="vault-select-swing" class="alfa-mini-btn" style="border-color:#caa14a; color:#caa14a;" title="Select vault stocks from swing list">Swing List</button>
<button id="vault-select-all" class="alfa-mini-btn" style="border-color:#caa14a; color:#caa14a;">Select All</button>
</div>
</div>
<div class="settings-row">
<div class="settings-section" style="flex:1;">
<div class="settings-section-title">Deposit Strategy</div>
<select id="vault-deposit-strategy" class="alfa-select" style="width:100%;">
<option value="swing_strategy" ${vaultConfig.depositStrategy === "swing_strategy" ? "selected" : ""}>Swing Strategy</option>
<option value="rsi_weighted" ${vaultConfig.depositStrategy === "rsi_weighted" ? "selected" : ""}>Weighted by RSI</option>
<option value="equal" ${vaultConfig.depositStrategy === "equal" ? "selected" : ""}>Equal Split</option>
<option value="dip_weighted" ${vaultConfig.depositStrategy === "dip_weighted" ? "selected" : ""}>Weighted by Dip</option>
<option value="roi_priority" ${vaultConfig.depositStrategy === "roi_priority" ? "selected" : ""}>ROI Priority</option>
</select>
</div>
<div class="settings-section" style="flex:1;">
<div class="settings-section-title">Withdrawal Strategy</div>
<select id="vault-withdraw-strategy" class="alfa-select" style="width:100%;">
<option value="proportional" ${vaultConfig.withdrawStrategy === "proportional" ? "selected" : ""}>Proportional</option>
<option value="loose_first" ${vaultConfig.withdrawStrategy === "loose_first" ? "selected" : ""}>Loose Shares First</option>
<option value="swing_strategy" ${vaultConfig.withdrawStrategy === "swing_strategy" ? "selected" : ""}>Swing Strategy</option>
<option value="worst_roi" ${vaultConfig.withdrawStrategy === "worst_roi" ? "selected" : ""}>Worst ROI First</option>
<option value="best_performers" ${vaultConfig.withdrawStrategy === "best_performers" ? "selected" : ""}>Best Performers</option>
</select>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">Default Keep Amount</div>
<input type="text" id="vault-keep-amount" class="alfa-input" style="width:150px;"
value="${vaultConfig.keepAmount > 0 ? vaultConfig.keepAmount.toLocaleString("en-US") : ""}"
placeholder="e.g. 1m, 500k">
<span style="font-size:11px; color:#666; margin-left:10px;">Cash to keep when using Vault Spread</span>
</div>
</div>
<!-- Protection Tab -->
<div class="settings-tab-content" id="settings-tab-protection" style="display:none;">
<div class="settings-section">
<div class="settings-section-title">Block Protection</div>
<label class="settings-checkbox-label">
<input type="checkbox" id="settings-lock-blocks" ${lockBlocksChecked ? "checked" : ""}>
<span><strong>Lock Blocks</strong> - Protect benefit tier shares from being sold during withdrawals and rebalancing</span>
</label>
<label class="settings-checkbox-label">
<input type="checkbox" id="settings-skip-overvalued" ${vaultConfig.skipOvervalued ? "checked" : ""}>
<span><strong>Smart Mode</strong> - Skip stocks with SELL or TAKE PROFIT signals when spreading/rebalancing</span>
</label>
<label class="settings-checkbox-label">
<input type="checkbox" id="settings-exclude-locked-from-value" ${vaultConfig.excludeLockedBlocksFromValue ? "checked" : ""}>
<span><strong>Exclude locked blocks from Total Vault Value</strong> - Show only loose + swing-protected value (locked block shares not counted)</span>
</label>
</div>
</div>
<!-- Modules Tab -->
<div class="settings-tab-content" id="settings-tab-modules" style="display:none;">
<div class="settings-section">
<div class="settings-section-title">Visible modules</div>
<div class="settings-note">Uncheck to hide sections on the main screen. Hidden modules are not loaded until shown (saves work on load).</div>
<label class="settings-checkbox-label"><input type="checkbox" id="settings-mod-portfolio" ${mod.portfolio ? "checked" : ""}> <span>Portfolio P&L</span></label>
<label class="settings-checkbox-label"><input type="checkbox" id="settings-mod-gamble" ${mod.gamble ? "checked" : ""}> <span>Gamble Module</span></label>
<label class="settings-checkbox-label"><input type="checkbox" id="settings-mod-swing" ${mod.swing ? "checked" : ""}> <span>Swing Trading</span></label>
<label class="settings-checkbox-label"><input type="checkbox" id="settings-mod-stocks" ${mod.stocks ? "checked" : ""}> <span>Vault Stocks (vault breakdown)</span></label>
</div>
<div class="settings-section">
<div class="settings-section-title">Display</div>
<label class="settings-checkbox-label">
<input type="checkbox" id="settings-minimal-mode" ${vaultConfig.minimalMode ? "checked" : ""}>
<span><strong>Minimal Mode</strong> - Show only Vault Spread, Rebalance, Withdraw, and options (Lock Blocks, Smart Mode). Use Torn's default stock display.</span>
</label>
</div>
</div>
<!-- Advanced Tab -->
<div class="settings-tab-content" id="settings-tab-advanced" style="display:none;">
<div class="settings-section">
<div class="settings-section-title">Bank ROI Rates (% APR)</div>
<div class="settings-note">Used for daily income calculations</div>
<div class="settings-bank-inputs">
${bankInputs}
</div>
<button id="settings-fetch-bank" class="alfa-mini-btn" style="width:100%; margin-top:10px; border-color:#caa14a; color:#caa14a;">Fetch Rates (API)</button>
</div>
<div class="settings-section">
<div class="settings-section-title">Cache Management</div>
<button id="settings-clear-cache" class="alfa-mini-btn" style="width:100%; border-color:#ef5350; color:#ef5350;">Clear Analysis Cache</button>
</div>
</div>
</div>
<div class="settings-actions">
<button id="settings-save" class="alfa-main-btn" style="flex:1; border-color:#8bc34a; color:#8bc34a;">Save & Close</button>
</div>`;
createModal("Settings", html);
$(".settings-tab").on("click", function() {
const tab = $(this).data("tab");
$(".settings-tab").removeClass("active");
$(this).addClass("active");
$(".settings-tab-content").hide();
$(`#settings-tab-${tab}`).show();
});
$("#settings-apikey").on("keyup change", function() {
localStorage.setItem("alfa_vault_apikey", $(this).val().trim());
if ($(this).val().trim()) {
$("#settings-api-status").html('<span style="color:#ffd54f;">Key updated - click Validate to test</span>');
} else {
$("#settings-api-status").html('<span style="color:#888;">No API key set</span>');
}
});
$("#settings-validate-key").on("click", async function() {
const key = localStorage.getItem("alfa_vault_apikey");
if (!key) {
$("#settings-api-status").html('<span style="color:#ef5350;">No API key to validate</span>');
return;
}
$(this).prop("disabled", true).text("Validating...");
try {
const res = await fetch(`https://api.torn.com/user/?selections=basic&key=${key}`);
const data = await res.json();
if (data.error) {
$("#settings-api-status").html(`<span style="color:#ef5350;">\u2717 Invalid: ${data.error.error}</span>`);
} else {
$("#settings-api-status").html(`<span style="color:#8bc34a;">\u2713 Valid key for ${data.name} [${data.player_id}]</span>`);
}
} catch (e) {
$("#settings-api-status").html('<span style="color:#ef5350;">\u2717 Connection error</span>');
}
$(this).prop("disabled", false).text("Validate API Key");
});
$(".vault-stock-check").on("change", function() {
const sym = $(this).val();
const isChecked = $(this).is(":checked");
$(this).closest(".vault-stock-option").toggleClass("selected", isChecked);
if (isChecked && !vaultConfig.stocks.includes(sym)) {
vaultConfig.stocks.push(sym);
} else if (!isChecked) {
vaultConfig.stocks = vaultConfig.stocks.filter((s) => s !== sym);
}
$("#vault-selected-count").text(vaultConfig.stocks.length);
});
$("#vault-select-none").on("click", function() {
$(".vault-stock-check").prop("checked", false);
$(".vault-stock-option").removeClass("selected");
vaultConfig.stocks = [];
$("#vault-selected-count").text(0);
});
$("#vault-select-active").on("click", function() {
$(".vault-stock-check").prop("checked", false);
$(".vault-stock-option").removeClass("selected");
vaultConfig.stocks = [];
Object.keys(STOCK_DATA).forEach((sym) => {
if (STOCK_DATA[sym].type === "A") {
$(`.vault-stock-check[value="${sym}"]`).prop("checked", true);
$(`.vault-stock-option[data-sym="${sym}"]`).addClass("selected");
vaultConfig.stocks.push(sym);
}
});
$("#vault-selected-count").text(vaultConfig.stocks.length);
});
$("#vault-select-swing").on("click", function() {
$(".vault-stock-check").prop("checked", false);
$(".vault-stock-option").removeClass("selected");
vaultConfig.stocks = [];
VAULT_SWING_PRESET_SYMBOLS.forEach((sym) => {
if ($(`.vault-stock-check[value="${sym}"]`).length) {
$(`.vault-stock-check[value="${sym}"]`).prop("checked", true);
$(`.vault-stock-option[data-sym="${sym}"]`).addClass("selected");
vaultConfig.stocks.push(sym);
}
});
$("#vault-selected-count").text(vaultConfig.stocks.length);
});
$("#vault-select-all").on("click", function() {
$(".vault-stock-check").prop("checked", false);
$(".vault-stock-option").removeClass("selected");
vaultConfig.stocks = [];
Object.keys(STOCK_DATA).forEach((sym) => {
$(`.vault-stock-check[value="${sym}"]`).prop("checked", true);
$(`.vault-stock-option[data-sym="${sym}"]`).addClass("selected");
vaultConfig.stocks.push(sym);
});
$("#vault-selected-count").text(vaultConfig.stocks.length);
});
$("#vault-deposit-strategy").on("change", function() {
vaultConfig.depositStrategy = $(this).val();
});
$("#vault-withdraw-strategy").on("change", function() {
vaultConfig.withdrawStrategy = $(this).val();
});
$("#vault-keep-amount").on("change keyup", function() {
vaultConfig.keepAmount = parseTornNumber($(this).val()) || 0;
});
$("#settings-lock-blocks").on("change", function() {
localStorage.setItem("alfa_vault_lock", $(this).is(":checked"));
});
$("#settings-skip-overvalued").on("change", function() {
vaultConfig.skipOvervalued = $(this).is(":checked");
});
$("#settings-minimal-mode").on("change", function() {
vaultConfig.minimalMode = $(this).is(":checked");
});
$(".alfa-tbl-input").on("change keyup", function() {
["1w", "2w", "1m", "2m", "3m"].forEach((t) => {
bankSettings["roi_" + t] = parseFloat($(`#bank-${t}`).val()) || 0;
});
});
$("#settings-fetch-bank").on("click", fetchBankRates);
$("#settings-clear-cache").on("click", function() {
if (confirm("Clear all cached analysis data?")) {
clearVaultCache();
vaultAnalysisCache = {};
$(this).text("Cleared!").css("color", "#8bc34a");
setTimeout(() => $(this).text("Clear Analysis Cache").css("color", "#ef5350"), 1500);
}
});
$("#settings-save").on("click", function() {
if ($("#settings-exclude-locked-from-value").length) vaultConfig.excludeLockedBlocksFromValue = $("#settings-exclude-locked-from-value").is(":checked");
if ($("#settings-mod-portfolio").length) {
vaultConfig.modules = {
portfolio: $("#settings-mod-portfolio").is(":checked"),
gamble: $("#settings-mod-gamble").is(":checked"),
swing: $("#settings-mod-swing").is(":checked"),
stocks: $("#settings-mod-stocks").is(":checked")
};
}
saveVaultConfig();
localStorage.setItem("alfa_advisor_networth", JSON.stringify(networthSettings));
localStorage.setItem("alfa_advisor_bank", JSON.stringify(bankSettings));
$(this).text("Saved!").css("background", "#8bc34a").css("color", "#111");
setTimeout(() => {
$("#alfa-modal-overlay").remove();
updateVaultDisplay();
}, 500);
});
}
function openVaultConfigModal() {
const allStocks = Object.keys(STOCK_DATA).sort();
const stockCheckboxes = allStocks.map((sym) => {
const isSelected = vaultConfig.stocks.includes(sym);
const stockData = STOCK_DATA[sym];
const typeLabel = stockData.type === "P" ? "P" : "A";
return `<label class="vault-stock-option ${isSelected ? "selected" : ""}" data-sym="${sym}">
<input type="checkbox" class="vault-stock-check" value="${sym}" ${isSelected ? "checked" : ""}>
<span class="vault-stock-sym">${sym}</span>
<span class="vault-stock-type">${typeLabel}</span>
</label>`;
}).join("");
const html = `
<div class="vault-config-container">
<div class="vault-config-section">
<div class="vault-config-header">
<span class="vault-config-title">Select Vault Stocks</span>
<span class="vault-stock-count">Selected: <strong id="vault-selected-count">${vaultConfig.stocks.length}</strong></span>
</div>
<div class="vault-stock-grid" id="vault-stock-grid">
${stockCheckboxes}
</div>
<div class="vault-quick-actions">
<button id="vault-select-none" class="alfa-mini-btn" style="border-color:#888; color:#888;">Clear All</button>
<button id="vault-select-active" class="alfa-mini-btn" style="border-color:#caa14a; color:#caa14a;">Select Active (A)</button>
<button id="vault-select-swing" class="alfa-mini-btn" style="border-color:#caa14a; color:#caa14a;" title="Select vault stocks from swing list">Swing List</button>
<button id="vault-select-all" class="alfa-mini-btn" style="border-color:#caa14a; color:#caa14a;">Select All</button>
</div>
</div>
<div class="vault-config-row">
<div class="vault-config-section" style="flex:1;">
<div class="vault-config-header">
<span class="vault-config-title">Deposit Strategy</span>
</div>
<select id="vault-deposit-strategy" class="alfa-select" style="width:100%;">
<option value="swing_strategy" ${vaultConfig.depositStrategy === "swing_strategy" ? "selected" : ""}>Swing Strategy (matches swing scan)</option>
<option value="rsi_weighted" ${vaultConfig.depositStrategy === "rsi_weighted" ? "selected" : ""}>Weighted by RSI (oversold priority)</option>
<option value="equal" ${vaultConfig.depositStrategy === "equal" ? "selected" : ""}>Equal Split</option>
<option value="dip_weighted" ${vaultConfig.depositStrategy === "dip_weighted" ? "selected" : ""}>Weighted by Dip (% below avg)</option>
<option value="roi_priority" ${vaultConfig.depositStrategy === "roi_priority" ? "selected" : ""}>ROI Priority (benefit earners)</option>
</select>
<div class="vault-strategy-desc" id="deposit-strategy-desc">Prioritize oversold stocks (RSI < 30 gets more allocation)</div>
</div>
<div class="vault-config-section" style="flex:1;">
<div class="vault-config-header">
<span class="vault-config-title">Withdrawal Strategy</span>
</div>
<select id="vault-withdraw-strategy" class="alfa-select" style="width:100%;">
<option value="proportional" ${vaultConfig.withdrawStrategy === "proportional" ? "selected" : ""}>Proportional (equal % from each)</option>
<option value="loose_first" ${vaultConfig.withdrawStrategy === "loose_first" ? "selected" : ""}>Loose Shares First (Recommended)</option>
<option value="swing_strategy" ${vaultConfig.withdrawStrategy === "swing_strategy" ? "selected" : ""}>Swing Strategy</option>
<option value="worst_roi" ${vaultConfig.withdrawStrategy === "worst_roi" ? "selected" : ""}>Worst ROI First</option>
<option value="best_performers" ${vaultConfig.withdrawStrategy === "best_performers" ? "selected" : ""}>Best Performers (take profits)</option>
</select>
<div class="vault-strategy-desc" id="withdraw-strategy-desc">Sell equal percentage from each stock</div>
</div>
</div>
<div class="vault-config-section">
<div class="vault-config-header">
<span class="vault-config-title">Default Keep Amount</span>
</div>
<div style="display:flex; gap:10px; align-items:center;">
<input type="text" id="vault-keep-amount" class="alfa-input" style="width:150px;"
value="${vaultConfig.keepAmount > 0 ? vaultConfig.keepAmount.toLocaleString("en-US") : ""}"
placeholder="e.g. 1m, 500k">
<span style="font-size:11px; color:#666;">Cash to keep when using Vault Spread</span>
</div>
</div>
<div class="vault-config-actions">
<button id="vault-config-clear-cache" class="alfa-main-btn" style="border-color:#ef5350; color:#ef5350;">Clear Cache</button>
<button id="vault-config-save" class="alfa-main-btn" style="border-color:#8bc34a; color:#8bc34a; flex:1;">Save Configuration</button>
</div>
</div>`;
createModal("Vault Configuration", html);
const depositDescs = {
"swing_strategy": "Same multi-indicator logic as swing scan (RSI, Bollinger, S/R, ADX, volume)",
"equal": "Distribute cash evenly across all vault stocks",
"dip_weighted": "Put more cash into stocks trading below their 7-day average",
"rsi_weighted": "Prioritize oversold stocks (RSI < 30 gets more allocation)",
"roi_priority": "Allocate based on benefit ROI (higher ROI = more cash)"
};
const withdrawDescs = {
"loose_first": "Sell shares above benefit thresholds first (protects tiers)",
"proportional": "Sell equal percentage from each vault stock",
"swing_strategy": "Sell first from stocks with SELL/TAKE PROFIT signals (same logic as swing scan)",
"worst_roi": "Sell from lowest ROI stocks first (keep the good ones)",
"best_performers": "Sell from stocks that are up (take profits)"
};
$("#deposit-strategy-desc").text(depositDescs[vaultConfig.depositStrategy] || depositDescs["swing_strategy"]);
$("#withdraw-strategy-desc").text(withdrawDescs[vaultConfig.withdrawStrategy] || withdrawDescs["proportional"]);
$(".vault-stock-check").on("change", function() {
const sym = $(this).val();
const isChecked = $(this).is(":checked");
$(this).closest(".vault-stock-option").toggleClass("selected", isChecked);
if (isChecked && !vaultConfig.stocks.includes(sym)) {
vaultConfig.stocks.push(sym);
} else if (!isChecked) {
vaultConfig.stocks = vaultConfig.stocks.filter((s) => s !== sym);
}
$("#vault-selected-count").text(vaultConfig.stocks.length);
});
$("#vault-select-none").on("click", function() {
$(".vault-stock-check").prop("checked", false);
$(".vault-stock-option").removeClass("selected");
vaultConfig.stocks = [];
$("#vault-selected-count").text(0);
});
$("#vault-select-active").on("click", function() {
$(".vault-stock-check").prop("checked", false);
$(".vault-stock-option").removeClass("selected");
vaultConfig.stocks = [];
Object.keys(STOCK_DATA).forEach((sym) => {
if (STOCK_DATA[sym].type === "A") {
$(`.vault-stock-check[value="${sym}"]`).prop("checked", true);
$(`.vault-stock-option[data-sym="${sym}"]`).addClass("selected");
vaultConfig.stocks.push(sym);
}
});
$("#vault-selected-count").text(vaultConfig.stocks.length);
});
$("#vault-select-all").on("click", function() {
$(".vault-stock-check").prop("checked", false);
$(".vault-stock-option").removeClass("selected");
vaultConfig.stocks = [];
Object.keys(STOCK_DATA).forEach((sym) => {
$(`.vault-stock-check[value="${sym}"]`).prop("checked", true);
$(`.vault-stock-option[data-sym="${sym}"]`).addClass("selected");
vaultConfig.stocks.push(sym);
});
$("#vault-selected-count").text(vaultConfig.stocks.length);
});
$("#vault-deposit-strategy").on("change", function() {
vaultConfig.depositStrategy = $(this).val();
$("#deposit-strategy-desc").text(depositDescs[vaultConfig.depositStrategy]);
});
$("#vault-withdraw-strategy").on("change", function() {
vaultConfig.withdrawStrategy = $(this).val();
$("#withdraw-strategy-desc").text(withdrawDescs[vaultConfig.withdrawStrategy]);
});
$("#vault-keep-amount").on("change keyup", function() {
vaultConfig.keepAmount = parseTornNumber($(this).val()) || 0;
});
$("#vault-config-clear-cache").on("click", function() {
if (confirm("Clear all cached Tornsy data?")) {
clearVaultCache();
vaultAnalysisCache = {};
$(this).text("Cleared!").css("color", "#8bc34a");
setTimeout(() => $(this).text("Clear Cache").css("color", "#ef5350"), 1500);
}
});
$("#vault-config-save").on("click", function() {
saveVaultConfig();
$(this).text("Saved!").css("background", "#8bc34a").css("color", "#111");
setTimeout(() => {
$(this).text("Save Configuration").css("background", "transparent").css("color", "#8bc34a");
$("#alfa-modal-overlay").remove();
updateVaultDisplay();
}, 800);
});
}
async function calculateDailyIncome() {
let stockIncome = 0;
let ownedBlocks = [];
const symbols = Object.keys(STOCK_DATA);
const IGNORED = ["BAG", "EVL", "CBD", "MCS"];
for (let sym of symbols) {
if (IGNORED.includes(sym)) continue;
let stockData = STOCK_DATA[sym];
let sharePrice = getPrice(sym);
if (sharePrice === 0) continue;
let owned = getOwnedShares(sym);
let increment = stockData.base;
let dailyYield = getDailyYield(sym);
if (sym === "PTS") {
let ptsPrice = itemPrices["points"] || 0;
if (ptsPrice > 0) dailyYield = ptsPrice * 100 / 7;
}
let currentLevel = 0;
if (stockData.type === "P") {
currentLevel = owned >= increment ? 1 : 0;
} else if (owned >= increment) {
currentLevel = Math.floor(Math.log2(owned / increment + 1));
}
if (currentLevel > 0 && dailyYield > 0) {
if (stockData.type === "P") {
let blockCost = increment * sharePrice;
let blockRoi = blockCost > 0 ? dailyYield * 365 / blockCost * 100 : 0;
stockIncome += dailyYield;
ownedBlocks.push({ name: sym, tier: 1, income: dailyYield, value: blockCost, roi: blockRoi });
} else {
for (let i = 1; i <= currentLevel; i++) {
let tierShares = increment * Math.pow(2, i - 1);
let tierCost = tierShares * sharePrice;
let tierRoi = tierCost > 0 ? dailyYield * 365 / tierCost * 100 : 0;
stockIncome += dailyYield;
ownedBlocks.push({ name: sym, tier: i, income: dailyYield, value: tierCost, roi: tierRoi });
}
}
}
}
let bankIncome = 0;
let bankPrincipal = 0;
let bankRate = 0;
const nwData = lastNwCache && lastNwCache.nwFromApi ? await getLiquidNetworth(true) : await getLiquidNetworth(false);
if (nwData && nwData.bankActive) {
bankIncome = nwData.dailyBank;
bankPrincipal = nwData.bankPrincipal;
if (bankPrincipal > 0) {
bankRate = bankIncome * 365 / bankPrincipal * 100;
}
}
return {
stockIncome,
bankIncome,
totalIncome: stockIncome + bankIncome,
bankPrincipal,
bankRate,
ownedBlocks
};
}
function getBlockStatus(sym) {
const stockData = STOCK_DATA[sym];
if (!stockData) return null;
const owned = getOwnedShares(sym);
const price = getPrice(sym);
const base = stockData.base;
const isPassive = stockData.type === "P";
let currentTier = 0;
if (isPassive) {
currentTier = owned >= base ? 1 : 0;
} else if (owned >= base) {
currentTier = Math.floor(Math.log2(owned / base + 1));
}
const maxTier = isPassive ? 1 : null;
const isMaxed = isPassive && currentTier >= 1;
let sharesForNextTier, nextTierTotal;
if (isPassive) {
sharesForNextTier = base;
nextTierTotal = base;
} else {
const nextTier = currentTier + 1;
nextTierTotal = base * (Math.pow(2, nextTier) - 1);
sharesForNextTier = base * Math.pow(2, nextTier - 1);
}
const sharesNeeded = Math.max(0, nextTierTotal - owned);
const progress = nextTierTotal > 0 ? Math.min(100, owned / nextTierTotal * 100) : 0;
const costToComplete = sharesNeeded * price;
const dailyYield = getDailyYield(sym);
const tierCost = sharesForNextTier * price;
const roi = tierCost > 0 ? dailyYield * 365 / tierCost * 100 : 0;
const userCash = getMoneyFast();
const canAfford = costToComplete <= userCash;
const isClose = progress >= 80 && progress < 100;
const hasOpportunity = !isMaxed && (isClose || canAfford) && sharesNeeded > 0;
return {
symbol: sym,
currentTier,
maxTier,
isMaxed,
isPassive,
sharesOwned: owned,
sharesForNextTier: nextTierTotal,
sharesNeeded,
progress,
costToComplete,
roi,
canAfford,
isClose,
hasOpportunity,
price,
dailyYield
};
}
function calculateTotalVaultValue() {
const excludeLocked = vaultConfig.excludeLockedBlocksFromValue === true;
const lockBlocks = $("#alfa-lock-toggle").length > 0 ? $("#alfa-lock-toggle").is(":checked") : localStorage.getItem("alfa_vault_lock") === "true";
let total = 0;
for (const sym of vaultConfig.stocks) {
const owned = getOwnedShares(sym);
const price = getPrice(sym);
if (!excludeLocked || !lockBlocks) {
total += owned * price;
continue;
}
const stockData = STOCK_DATA[sym];
let benefitLockedShares = 0;
if (stockData) {
const tierInfo = getBenefitTier(sym, owned);
if (stockData.type === "P") {
benefitLockedShares = owned >= stockData.base ? stockData.base : 0;
} else {
const currentTier = tierInfo.tier;
if (currentTier > 0) {
const minSharesForCurrentTier = stockData.base * (Math.pow(2, currentTier) - 1);
benefitLockedShares = Math.min(minSharesForCurrentTier, owned);
}
}
}
total += Math.max(0, owned - benefitLockedShares) * price;
}
return total;
}
function calculateVaultBreakdown() {
const breakdown = [];
const totalValue = calculateTotalVaultValue();
const excludeLocked = vaultConfig.excludeLockedBlocksFromValue === true;
const checkbox = $("#alfa-lock-toggle");
const lockBlocks = checkbox.length > 0 ? checkbox.is(":checked") : localStorage.getItem("alfa_vault_lock") === "true";
for (const sym of vaultConfig.stocks) {
const owned = getOwnedShares(sym);
const price = getPrice(sym);
const tierInfo = getBenefitTier(sym, owned);
const stockData = STOCK_DATA[sym];
const swingProtectedShares = swingTradeData.activePositions.filter((p) => (p.symbol || "").toUpperCase() === (sym || "").toUpperCase()).reduce((sum, p) => sum + (p.shares || 0), 0);
let benefitLockedShares = 0;
if (lockBlocks && stockData) {
if (stockData.type === "P") {
benefitLockedShares = owned >= stockData.base ? stockData.base : 0;
} else {
const currentTier = tierInfo.tier;
if (currentTier > 0) {
const minSharesForCurrentTier = stockData.base * (Math.pow(2, currentTier) - 1);
benefitLockedShares = Math.min(minSharesForCurrentTier, owned);
}
}
}
const totalProtected = benefitLockedShares + swingProtectedShares;
const looseShares = Math.max(0, owned - totalProtected);
const looseValue = looseShares * price;
const isLocked = vaultConfig.lockedStocks && vaultConfig.lockedStocks.includes(sym);
let value = owned * price;
if (excludeLocked && lockBlocks) {
value = Math.max(0, owned - benefitLockedShares) * price;
}
const percentage = totalValue > 0 ? value / totalValue * 100 : 0;
breakdown.push({
symbol: sym,
owned,
price,
value,
percentage,
tier: tierInfo.tier,
lockedShares: totalProtected,
benefitLockedShares,
swingProtectedShares,
looseShares,
looseValue,
isLocked,
hasSwingTrade: swingProtectedShares > 0,
analysis: vaultAnalysisCache[sym] || null
});
}
return breakdown.sort((a, b) => b.value - a.value);
}
function formatCompactNumber(num) {
const n = Number(num);
if (!isFinite(n)) return "0";
const abs = Math.abs(n);
if (abs >= 1e9) return (n / 1e9).toFixed(2) + "B";
if (abs >= 1e6) return (n / 1e6).toFixed(2) + "M";
if (abs >= 1e3) return (n / 1e3).toFixed(2) + "K";
return n.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 2 });
}
function getSignalBadge(analysis) {
if (!analysis) return "";
const signals = {
"buy": { color: "#66bb6a", text: "BUY", icon: "\u25B2" },
"good": { color: "#81c784", text: "GOOD", icon: "\u25B2" },
"hold": { color: "#ffd54f", text: "HOLD", icon: "\u25CF" },
"take_profit": { color: "#ffb74d", text: "TAKE PROFIT", icon: "\u25BC" },
"sell": { color: "#ef5350", text: "SELL", icon: "\u25BC" }
};
const sig = signals[analysis.signal] || signals.hold;
return `<span class="vault-signal-badge" style="background:${sig.color}20; color:${sig.color}; border-color:${sig.color};">${sig.icon} ${sig.text}</span>`;
}
function persistVaultModuleExpandedState() {
if ($("#vault-trade-view").is(":visible") || $("#vault-main-content").length && !$("#vault-main-content").is(":visible")) {
return;
}
if ($("#vault-pl-content").length) localStorage.setItem("alfa_pl_expanded", $("#vault-pl-content").is(":visible") ? "true" : "false");
if ($("#gamble-content").length) localStorage.setItem("alfa_gamble_expanded", $("#gamble-content").is(":visible") ? "true" : "false");
if ($("#swing-content").length) localStorage.setItem("alfa_swing_expanded", $("#swing-content").is(":visible") ? "true" : "false");
if ($("#stocks-content").length) localStorage.setItem("alfa_stocks_expanded", $("#stocks-content").is(":visible") ? "true" : "false");
}
function renderVaultSection() {
persistVaultModuleExpandedState();
$("#alfa-vault-section").remove();
const savedKey = localStorage.getItem("alfa_vault_apikey") || "";
const lockBlocks = localStorage.getItem("alfa_vault_lock") === "true";
if (vaultConfig.stocks.length === 0) {
const html2 = `
<div id="alfa-vault-section" class="alfa-vault-section">
<div class="alfa-vault-header">
<span class="alfa-vault-title">Smart Stock Vault</span>
<div class="alfa-vault-actions">
<button id="vault-config-btn" class="alfa-mini-btn" style="border-color:#caa14a; color:#caa14a;">Settings</button>
</div>
</div>
<div class="alfa-vault-empty">
<div style="font-size:14px; margin-bottom:8px;">No vault stocks configured</div>
<div style="font-size:11px; color:#666;">Click Settings to configure your vault</div>
</div>
</div>`;
mountVaultHtml(html2);
return;
}
const minimalMode = vaultConfig.minimalMode === true;
const mod = vaultConfig.modules || { ...DEFAULT_VAULT_CONFIG.modules };
const totalValue = calculateTotalVaultValue();
const breakdown = calculateVaultBreakdown();
const lockBlocksChecked = localStorage.getItem("alfa_vault_lock") === "true";
let plData, hasPortfolioData, lastSyncDate, currentPeriod, periodPL;
if (!minimalMode && (mod.portfolio || mod.stocks)) {
plData = calculatePortfolioPL();
hasPortfolioData = portfolioData.transactions.length > 0;
lastSyncDate = portfolioData.lastFullSync > 0 ? new Date(portfolioData.lastFullSync).toLocaleDateString() : "Never";
currentPeriod = localStorage.getItem("alfa_pl_period") || "all";
periodPL = calculatePeriodPL(currentPeriod);
}
const stockBars = !minimalMode && mod.stocks ? breakdown.map((stock) => {
const barWidth = Math.max(5, stock.percentage);
const dipDisplay = stock.analysis ? `<span class="${stock.analysis.dipFrom7d < 0 ? "vault-dip-neg" : "vault-dip-pos"}">${stock.analysis.dipFrom7d >= 0 ? "+" : ""}${stock.analysis.dipFrom7d.toFixed(1)}%</span>` : "";
const rsiDisplay = stock.analysis ? `<span class="vault-rsi ${stock.analysis.rsi < 30 ? "oversold" : stock.analysis.rsi > 70 ? "overbought" : ""}">RSI:${stock.analysis.rsi.toFixed(0)}</span>` : "";
const lockedClass = stock.isLocked ? "vault-row-locked" : "";
const stockPL = plData.perStock[stock.symbol];
let plDisplay = "";
if (stockPL && stockPL.totalCost > 0) {
const plColor = stockPL.unrealizedPL >= 0 ? "#8bc34a" : "#ef5350";
const plSign = stockPL.unrealizedPL >= 0 ? "+" : "";
plDisplay = `<span class="vault-stock-pl" style="color:${plColor};">${plSign}${stockPL.unrealizedPLPercent.toFixed(1)}%</span>`;
}
const swingIndicator = stock.hasSwingTrade ? `<span class="vault-swing-badge" title="Protected by swing trade (${stock.swingProtectedShares.toLocaleString()} shares)">\u{1F4C8}</span>` : "";
const blockStatus = getBlockStatus(stock.symbol);
let blockIcon = "";
let blockProgressBar = "";
if (blockStatus) {
const tierLabel = blockStatus.currentTier > 0 ? blockStatus.isPassive ? "1" : blockStatus.currentTier : "";
let iconClass = "no-block";
let iconTitle = "No block - click to complete";
if (blockStatus.isMaxed) {
iconClass = "maxed";
iconTitle = "Maximum tier reached \u2713";
} else if (blockStatus.canAfford) {
iconClass = "can-afford";
iconTitle = `Can afford next tier! Cost: ${formatMoney(blockStatus.costToComplete)}`;
} else if (blockStatus.currentTier > 0) {
iconClass = blockStatus.isClose ? "has-tier close" : "has-tier";
iconTitle = `Tier ${blockStatus.currentTier} - ${blockStatus.progress.toFixed(0)}% to next (${formatMoney(blockStatus.costToComplete)})`;
} else if (blockStatus.isClose) {
iconClass = "close";
iconTitle = `${blockStatus.progress.toFixed(0)}% to first tier (${formatMoney(blockStatus.costToComplete)})`;
}
blockIcon = `
<span class="vault-block-icon ${iconClass}"
data-sym="${stock.symbol}"
title="${iconTitle}">
<span class="block-icon-shape">\u25A3</span>
${tierLabel ? `<span class="block-tier-num">${tierLabel}</span>` : ""}
</span>`;
if (!blockStatus.isMaxed && blockStatus.progress > 0 && blockStatus.progress < 100) {
blockProgressBar = `
<div class="vault-block-progress" title="${blockStatus.sharesOwned.toLocaleString()} / ${blockStatus.sharesForNextTier.toLocaleString()} shares">
<div class="vault-block-progress-fill ${blockStatus.isClose ? "close" : ""}" style="width:${blockStatus.progress}%;"></div>
</div>`;
}
}
return `
<div class="vault-stock-row ${lockedClass}" data-sym="${stock.symbol}">
<div class="vault-stock-info">
<span class="vault-stock-name">${stock.symbol}</span>
${swingIndicator}
<span class="vault-stock-value">$${formatCompactNumber(stock.value)}</span>
<span class="vault-stock-pct">(${stock.percentage.toFixed(1)}%)</span>
${plDisplay}
${getSignalBadge(stock.analysis)}
<div class="vault-stock-actions">
<button class="vault-action-btn vault-quick-buy" data-sym="${stock.symbol}" title="Quick buy">Buy</button>
<button class="vault-action-btn vault-quick-sell" data-sym="${stock.symbol}" title="Sell loose shares" ${stock.looseShares <= 0 ? "disabled" : ""}>Sell</button>
<button class="vault-action-btn vault-toggle-lock ${stock.isLocked ? "locked" : ""}" data-sym="${stock.symbol}" title="${stock.isLocked ? "Unlock stock" : "Lock stock"}">${stock.isLocked ? "\u{1F512}" : "\u{1F513}"}</button>
${blockIcon}
</div>
</div>
<div class="vault-stock-bar-container">
<div class="vault-stock-bar" style="width:${barWidth}%;">
<div class="vault-stock-bar-loose" style="width:${stock.looseShares > 0 ? stock.looseValue / stock.value * 100 : 0}%;"></div>
</div>
</div>
${blockProgressBar}
<div class="vault-stock-details">
${dipDisplay} ${rsiDisplay}
${stock.looseShares > 0 ? `<span class="vault-loose">Loose: $${formatCompactNumber(stock.looseValue)}</span>` : ""}
</div>
</div>`;
}).join("") : "";
const html = `
<div id="alfa-vault-section" class="alfa-vault-section">
<div class="alfa-vault-header">
<span class="alfa-vault-title">Smart Stock Vault</span>
<div class="alfa-vault-actions">
<button id="vault-config-btn" class="alfa-mini-btn" style="border-color:#888; color:#888;">Settings</button>
</div>
</div>
<div id="vault-main-content">
${!minimalMode ? `
<div class="alfa-vault-summary">
<div class="alfa-vault-total">
<span class="alfa-vault-total-label">Total Vault Value</span>
<span class="alfa-vault-total-value">${formatMoneyWhole(totalValue)}</span>
</div>
<div class="alfa-vault-meta">
<span>${vaultConfig.stocks.length} stocks</span>
</div>
</div>
<div class="alfa-vault-income-row" id="vault-income-row">
<span class="vault-income-icon">\u{1F4B0}</span>
<span class="vault-income-label">Daily Income:</span>
<span class="vault-income-value" id="vault-daily-income">--</span>
<span class="vault-income-sep">|</span>
<span class="vault-income-bank" id="vault-bank-info">\u{1F3E6} Loading...</span>
</div>
${mod.portfolio ? `
<div class="alfa-vault-pl-section" id="vault-pl-section">
<div class="vault-pl-header" id="vault-pl-toggle">
<span class="vault-pl-title">\u{1F4CA} Portfolio P&L</span>
<span class="alfa-caret ${localStorage.getItem("alfa_pl_expanded") !== "false" ? "rotated" : ""}">\u25BC</span>
</div>
<div class="vault-pl-content" id="vault-pl-content" style="display:${localStorage.getItem("alfa_pl_expanded") !== "false" ? "block" : "none"};">
${hasPortfolioData ? `
<div class="vault-pl-period-filters">
<button class="pl-period-btn ${currentPeriod === "day" ? "active" : ""}" data-period="day">Day</button>
<button class="pl-period-btn ${currentPeriod === "week" ? "active" : ""}" data-period="week">Week</button>
<button class="pl-period-btn ${currentPeriod === "month" ? "active" : ""}" data-period="month">Month</button>
<button class="pl-period-btn ${currentPeriod === "ytd" ? "active" : ""}" data-period="ytd">YTD</button>
<button class="pl-period-btn ${currentPeriod === "year" ? "active" : ""}" data-period="year">Year</button>
<button class="pl-period-btn ${currentPeriod === "all" ? "active" : ""}" data-period="all">All</button>
</div>
<div class="vault-pl-period-summary">
<div class="pl-period-title">${getPeriodLabel(currentPeriod)} Activity</div>
<div class="pl-period-stats">
<div class="pl-period-stat">
<span class="pl-stat-label">Buys</span>
<span class="pl-stat-value">${periodPL.buys.length} (${formatMoneyWhole(periodPL.totalBuyValue)})</span>
</div>
<div class="pl-period-stat">
<span class="pl-stat-label">Sells</span>
<span class="pl-stat-value">${periodPL.sells.length} (${formatMoneyWhole(periodPL.totalSellValue)})</span>
</div>
<div class="pl-period-stat ${periodPL.realizedPL >= 0 ? "pl-positive" : "pl-negative"}">
<span class="pl-stat-label">Realized P&L</span>
<span class="pl-stat-value">${periodPL.realizedPL >= 0 ? "+" : ""}${formatMoneyWhole(periodPL.realizedPL)}</span>
</div>
<div class="pl-period-stat">
<span class="pl-stat-label">Net Flow</span>
<span class="pl-stat-value" style="color:${periodPL.netInvested > 0 ? "#66bb6a" : periodPL.netInvested < 0 ? "#ef5350" : "#888"};">${periodPL.netInvested > 0 ? "+" : ""}${formatMoneyWhole(periodPL.netInvested)}</span>
</div>
</div>
</div>
<div class="vault-pl-divider"></div>
<div class="vault-pl-section-title">Portfolio Totals</div>
<div class="vault-pl-grid">
<div class="vault-pl-item">
<span class="vault-pl-label">Cost Basis</span>
<span class="vault-pl-value">${plData.totalCostBasis > 0 ? formatMoneyWhole(plData.totalCostBasis) : "Unknown"}</span>
</div>
<div class="vault-pl-item">
<span class="vault-pl-label">Current Value</span>
<span class="vault-pl-value">${formatMoneyWhole(plData.totalCurrentValue)}</span>
</div>
<div class="vault-pl-item ${plData.unrealizedPL >= 0 ? "pl-positive" : "pl-negative"}">
<span class="vault-pl-label" ${plData.hasIncompleteData ? 'title="Cost basis may be incomplete when older buys are outside Torn log history"' : ""}>Unrealized P&L</span>
<span class="vault-pl-value">${plData.totalCostBasis > 0 ? (plData.unrealizedPL >= 0 ? "+" : "") + formatMoneyWhole(plData.unrealizedPL) + " (" + (plData.unrealizedPLPercent >= 0 ? "+" : "") + plData.unrealizedPLPercent.toFixed(2) + "%)" : "N/A"}</span>
</div>
<div class="vault-pl-item ${plData.realizedPL >= 0 ? "pl-positive" : "pl-negative"}">
<span class="vault-pl-label">Realized P&L \u2713</span>
<span class="vault-pl-value">${plData.realizedPL >= 0 ? "+" : ""}${formatMoneyWhole(plData.realizedPL)}</span>
</div>
<div class="vault-pl-item">
<span class="vault-pl-label">Total Fees</span>
<span class="vault-pl-value" style="color:#ff9800;">${formatMoneyWhole(plData.totalFees)}</span>
</div>
<div class="vault-pl-item">
<span class="vault-pl-label">Transactions</span>
<span class="vault-pl-value">${portfolioData.transactions.length}</span>
</div>
</div>
<div class="vault-pl-actions">
<button id="vault-pl-sync" class="alfa-mini-btn" style="border-color:#caa14a; color:#caa14a;">Sync New</button>
<button id="vault-pl-full-sync" class="alfa-mini-btn" style="border-color:#ff9800; color:#ff9800;">Full Sync</button>
<button id="vault-pl-history" class="alfa-mini-btn" style="border-color:#888; color:#888;">History</button>
<span class="vault-pl-sync-info">Last: ${lastSyncDate}</span>
</div>
` : `
<div class="vault-pl-empty">
<div style="margin-bottom:10px;">No portfolio data yet</div>
<button id="vault-pl-initial-sync" class="alfa-main-btn" style="border-color:#caa14a; color:#caa14a;">
Sync Transaction History
</button>
<div style="font-size:10px; color:#666; margin-top:8px;">
This will fetch your stock buy/sell history from Torn's logs
</div>
</div>
`}
</div>
</div>
` : ""}
${!minimalMode && mod.gamble ? `
<div class="alfa-gamble-section" id="gamble-section">
<div class="gamble-header" id="gamble-toggle">
<span class="gamble-title">\u{1F3B0} Gamble Module</span>
<span class="alfa-caret ${localStorage.getItem("alfa_gamble_expanded") !== "false" ? "rotated" : ""}">\u25BC</span>
</div>
<div class="gamble-content" id="gamble-content" style="display:${localStorage.getItem("alfa_gamble_expanded") !== "false" ? "block" : "none"};">
<div class="gamble-target-row">
<label class="gamble-target-label">Target Stock</label>
<select id="gamble-target-stock" class="alfa-select gamble-target-select">
${vaultConfig.stocks.map((s) => `<option value="${s}" ${getGambleTargetStock() === s ? "selected" : ""}>${s}</option>`).join("")}
</select>
<button id="gamble-config-btn" class="alfa-mini-btn" style="border-color:#888; color:#888;" title="Configure preset amounts">Configure</button>
</div>
<div class="gamble-presets-row">
<div class="gamble-presets-group">
<div class="gamble-cash-row">
<span class="gamble-cash-label">Cash:</span>
<span id="gamble-cash-value" class="gamble-cash-value">--</span>
</div>
<div class="gamble-presets-label">Deposit</div>
<div class="gamble-presets-btns">
${(gambleConfig.depositPresets || DEFAULT_GAMBLE_CONFIG.depositPresets).map(
(p, i) => `<button class="gamble-preset-btn gamble-deposit" data-amount="${p.amount}" data-label="${p.label}">${p.label}</button>`
).join("")}
</div>
</div>
<div class="gamble-presets-group">
<div class="gamble-cash-row">
<span class="gamble-cash-label">Available:</span>
<span id="gamble-available-value" class="gamble-available-value">--</span>
</div>
<div class="gamble-presets-label">Withdraw</div>
<div class="gamble-presets-btns">
${(gambleConfig.withdrawPresets || DEFAULT_GAMBLE_CONFIG.withdrawPresets).map(
(p, i) => `<button class="gamble-preset-btn gamble-withdraw" data-amount="${p.amount}" data-label="${p.label}">${p.label}</button>`
).join("")}
</div>
</div>
</div>
<div class="gamble-panic-row">
<button id="gamble-panic-btn" class="gamble-panic-btn">\u{1F6A8} Panic: Deposit All</button>
</div>
<div id="gamble-status" class="gamble-status"></div>
</div>
</div>
` : ""}
${!minimalMode && mod.swing ? `
<div class="alfa-swing-section" id="swing-section">
<div class="swing-header" id="swing-toggle">
<span class="swing-title">\u{1F4C8} Swing Trading</span>
<span class="alfa-caret ${localStorage.getItem("alfa_swing_expanded") !== "false" ? "rotated" : ""}">\u25BC</span>
</div>
<div class="swing-content" id="swing-content" style="display:${localStorage.getItem("alfa_swing_expanded") !== "false" ? "block" : "none"};">
<div class="swing-tabs">
<button class="swing-tab active" data-tab="positions">Positions</button>
<button class="swing-tab" data-tab="signals">Signals</button>
<button class="swing-tab" data-tab="journal">Journal</button>
</div>
<div class="swing-tab-content" id="swing-tab-positions">
<div class="swing-positions-list" id="swing-positions-list">
${renderSwingPositions()}
</div>
<div class="swing-actions">
<button id="swing-add-position" class="alfa-mini-btn" style="border-color:#66bb6a; color:#66bb6a;">+ Add Position</button>
<button id="swing-refresh-signals" class="alfa-mini-btn" style="border-color:#ffd54f; color:#ffd54f;" title="Update prices and signals for your positions">Refresh signals</button>
</div>
</div>
<div class="swing-tab-content" id="swing-tab-signals" style="display:none;">
<div class="swing-signals-list" id="swing-signals-list">
<div class="swing-loading">Click "Scan" to find opportunities...</div>
</div>
<div class="swing-actions">
<button id="swing-scan-signals" class="alfa-mini-btn" style="border-color:#ffd54f; color:#ffd54f;">\u{1F50D} Scan All Stocks</button>
</div>
</div>
<div class="swing-tab-content" id="swing-tab-journal" style="display:none;">
<div class="swing-stats" id="swing-stats">
${renderSwingStats()}
</div>
<div class="swing-journal-list" id="swing-journal-list">
${renderSwingJournal()}
</div>
</div>
</div>
</div>
` : ""}
${!minimalMode && mod.stocks ? `
<div class="alfa-stocks-section" id="stocks-section">
<div class="stocks-header" id="stocks-toggle">
<span class="stocks-title">\u{1F4CB} Vault Stocks</span>
<span class="alfa-caret ${localStorage.getItem("alfa_stocks_expanded") !== "false" ? "rotated" : ""}" id="stocks-caret">\u25BC</span>
</div>
<div class="stocks-content" id="stocks-content" style="display:${localStorage.getItem("alfa_stocks_expanded") !== "false" ? "block" : "none"};">
<div class="alfa-vault-breakdown" id="vault-breakdown">
${stockBars}
</div>
</div>
</div>
` : ""}
` : ""}
<div class="alfa-vault-controls">
<div class="alfa-vault-control-group">
<button id="vault-spread-btn" class="alfa-main-btn vault-spread-btn">Vault Spread</button>
<input type="text" id="vault-spread-keep" class="alfa-input" style="width:80px;" placeholder="Keep"
value="${vaultConfig.keepAmount > 0 ? vaultConfig.keepAmount : ""}">
</div>
<div class="alfa-vault-control-group">
<button id="vault-rebalance-btn" class="alfa-main-btn vault-rebalance-btn" title="Rebalance portfolio based on current strategy">Rebalance</button>
${!minimalMode && mod.stocks ? `<button id="vault-analyze-btn" class="alfa-main-btn vault-analyze-btn" title="Refresh analysis and update signals on vault stocks">Analyze</button>` : ""}
</div>
<div class="alfa-vault-control-group">
<input type="text" id="vault-withdraw-amt" class="alfa-input" style="width:100px;" placeholder="Amount">
<button id="vault-withdraw-btn" class="alfa-main-btn vault-withdraw-btn">Withdraw</button>
</div>
</div>
<div class="alfa-vault-options">
<label class="vault-option-label" title="Protect benefit tier shares from being sold">
<input type="checkbox" id="alfa-lock-toggle" ${lockBlocksChecked ? "checked" : ""}>
<span>Lock Blocks</span>
</label>
<label class="vault-option-label" title="Skip stocks with SELL or TAKE PROFIT signals">
<input type="checkbox" id="vault-skip-overvalued" ${vaultConfig.skipOvervalued ? "checked" : ""}>
<span>Smart Mode</span>
</label>
</div>
<div id="vault-status" class="alfa-vault-status"></div>
</div>
<div id="vault-trade-view" class="vault-trade-view" style="display:none;"></div>
</div>`;
mountVaultHtml(html);
$("#vault-spread-btn").on("click", vaultSpread);
$("#vault-rebalance-btn").on("click", vaultRebalance);
$("#vault-withdraw-btn").on("click", () => smartWithdraw(parseTornNumber($("#vault-withdraw-amt").val())));
$("#vault-analyze-btn").on("click", vaultAnalyze);
$("#vault-pl-toggle").on("click", function() {
$("#vault-pl-content").slideToggle(150, function() {
localStorage.setItem("alfa_pl_expanded", $("#vault-pl-content").is(":visible") ? "true" : "false");
});
$(this).find(".alfa-caret").toggleClass("rotated");
});
$(".pl-period-btn").on("click", function() {
const period = $(this).data("period");
localStorage.setItem("alfa_pl_period", period);
$(".pl-period-btn").removeClass("active");
$(this).addClass("active");
const periodPL2 = calculatePeriodPL(period);
$(".pl-period-title").text(getPeriodLabel(period) + " Activity");
$(".pl-period-stats").html(`
<div class="pl-period-stat">
<span class="pl-stat-label">Buys</span>
<span class="pl-stat-value">${periodPL2.buys.length} (${formatMoneyWhole(periodPL2.totalBuyValue)})</span>
</div>
<div class="pl-period-stat">
<span class="pl-stat-label">Sells</span>
<span class="pl-stat-value">${periodPL2.sells.length} (${formatMoneyWhole(periodPL2.totalSellValue)})</span>
</div>
<div class="pl-period-stat ${periodPL2.realizedPL >= 0 ? "pl-positive" : "pl-negative"}">
<span class="pl-stat-label">Realized P&L</span>
<span class="pl-stat-value">${periodPL2.realizedPL >= 0 ? "+" : ""}${formatMoneyWhole(periodPL2.realizedPL)}</span>
</div>
<div class="pl-period-stat">
<span class="pl-stat-label">Net Flow</span>
<span class="pl-stat-value" style="color:${periodPL2.netInvested > 0 ? "#66bb6a" : periodPL2.netInvested < 0 ? "#ef5350" : "#888"};">${periodPL2.netInvested > 0 ? "+" : ""}${formatMoneyWhole(periodPL2.netInvested)}</span>
</div>
`);
});
$("#vault-pl-sync, #vault-pl-initial-sync").on("click", async function() {
const btn = $(this);
const status = $("#vault-status");
btn.prop("disabled", true).text("Syncing...");
const syncResult = await incrementalPortfolioSync((msg) => {
status.html(`<span style="color:#ffd54f;">${msg}</span>`);
});
updateVaultDisplay();
const statusAfter = $("#vault-status");
if (syncResult && !syncResult.success) {
statusAfter.html(`<span style="color:#ef5350;">${syncResult.error || "Sync failed"}</span>`);
}
setTimeout(() => statusAfter.html(""), 4e3);
});
$("#vault-pl-full-sync").on("click", async function() {
if (!confirm("Full sync will fetch ALL your stock transaction history. This may take a while. Continue?")) {
return;
}
const btn = $(this);
const status = $("#vault-status");
btn.prop("disabled", true).text("Syncing...");
const syncResult = await fullPortfolioSync((msg) => {
status.html(`<span style="color:#ffd54f;">${msg}</span>`);
});
updateVaultDisplay();
const statusAfter = $("#vault-status");
if (!syncResult.success) {
statusAfter.html(`<span style="color:#ef5350;">${syncResult.error || "Sync failed"}</span>`);
}
setTimeout(() => statusAfter.html(""), 4e3);
});
$("#vault-pl-history").on("click", openTransactionHistoryModal);
$("#gamble-toggle").on("click", function() {
$("#gamble-content").slideToggle(150, function() {
localStorage.setItem("alfa_gamble_expanded", $("#gamble-content").is(":visible") ? "true" : "false");
if ($("#gamble-content").is(":visible")) updateGambleCashDisplay();
});
$(this).find(".alfa-caret").toggleClass("rotated");
});
$("#gamble-target-stock").on("change", function() {
gambleConfig.targetStock = $(this).val();
saveGambleConfig();
updateGambleCashDisplay();
});
$("#gamble-config-btn").on("click", openGambleConfigModal);
$(document).off("click.gambleDeposit", ".gamble-preset-btn.gamble-deposit").on("click.gambleDeposit", ".gamble-preset-btn.gamble-deposit", async function() {
const amount = parseInt($(this).data("amount"), 10);
const statusEl = $("#gamble-status");
await executeGambleDeposit(amount, $(this), statusEl);
});
$(document).off("click.gambleWithdraw", ".gamble-preset-btn.gamble-withdraw").on("click.gambleWithdraw", ".gamble-preset-btn.gamble-withdraw", async function() {
const amount = parseInt($(this).data("amount"), 10);
const statusEl = $("#gamble-status");
await executeGambleWithdraw(amount, $(this), statusEl);
});
$("#gamble-panic-btn").on("click", async function() {
const statusEl = $("#gamble-status");
await executeGamblePanic($(this), statusEl);
});
if (localStorage.getItem("alfa_gamble_expanded") !== "false") {
updateGambleCashDisplay();
}
$("#swing-toggle").on("click", function() {
$("#swing-content").slideToggle(150, function() {
localStorage.setItem("alfa_swing_expanded", $("#swing-content").is(":visible") ? "true" : "false");
});
$(this).find(".alfa-caret").toggleClass("rotated");
});
$("#stocks-toggle").on("click", function() {
$("#stocks-content").slideToggle(150, function() {
localStorage.setItem("alfa_stocks_expanded", $("#stocks-content").is(":visible") ? "true" : "false");
});
$(this).find(".alfa-caret").toggleClass("rotated");
});
$(".swing-tab").on("click", function() {
const tab = $(this).data("tab");
$(".swing-tab").removeClass("active");
$(this).addClass("active");
$(".swing-tab-content").hide();
$(`#swing-tab-${tab}`).show();
if (tab === "journal") bindSwingJournalActions();
});
$("#swing-add-position").on("click", openAddPositionModal);
$("#swing-refresh-signals").on("click", async function() {
const btn = $(this);
const positions = swingTradeData.activePositions;
if (positions.length === 0) return;
btn.prop("disabled", true).text("Updating...");
try {
updateAllPositionPrices();
await refreshPositionSignals();
$("#swing-positions-list").html(renderSwingPositions());
bindSwingPositionActions();
} catch (e) {
console.error("Refresh signals error:", e);
}
btn.prop("disabled", false).text("Refresh signals");
});
$("#swing-scan-signals").on("click", async function() {
const btn = $(this);
btn.prop("disabled", true).text("Scanning...");
try {
const { all } = await getTopSwingSignals(10);
$("#swing-signals-list").html(renderSwingSignals(all));
$("#swing-positions-list").html(renderSwingPositions());
bindSwingSignalActions();
bindSwingPositionActions();
} catch (e) {
console.error("Signal scan error:", e);
$("#swing-signals-list").html(`<div class="swing-error">Error scanning: ${e.message}</div>`);
}
btn.prop("disabled", false).text("\u{1F50D} Scan All Stocks");
});
bindSwingPositionActions();
$("#vault-spread-keep").on("change", function() {
vaultConfig.keepAmount = parseTornNumber($(this).val()) || 0;
saveVaultConfig();
});
$("#alfa-lock-toggle").on("change", function() {
localStorage.setItem("alfa_vault_lock", $(this).is(":checked"));
updateVaultDisplay();
});
$("#vault-skip-overvalued").on("change", function() {
vaultConfig.skipOvervalued = $(this).is(":checked");
saveVaultConfig();
});
$(".vault-quick-buy").on("click", function(e) {
e.stopPropagation();
const sym = $(this).data("sym");
vaultQuickBuy(sym);
});
$(".vault-quick-sell").on("click", function(e) {
e.stopPropagation();
const sym = $(this).data("sym");
vaultQuickSell(sym);
});
$(".vault-toggle-lock").on("click", function(e) {
e.stopPropagation();
const sym = $(this).data("sym");
toggleStockLock(sym);
});
$(".vault-block-icon").on("click", function(e) {
e.stopPropagation();
const sym = $(this).data("sym");
openQuickBlockModal(sym);
});
loadDailyIncomeDisplay();
}
function openQuickBlockModal(sym) {
const blockStatus = getBlockStatus(sym);
if (!blockStatus) {
alert("Unable to get block status for " + sym);
return;
}
const stockData = STOCK_DATA[sym];
const tierType = stockData.type === "P" ? "Passive" : "Active";
const currentTierDisplay = blockStatus.currentTier > 0 ? blockStatus.isPassive ? "Passive Block" : `Tier ${blockStatus.currentTier}` : "No Block";
const nextTierDisplay = blockStatus.isPassive ? "Passive Block" : `Tier ${blockStatus.currentTier + 1}`;
const progressWidth = Math.min(100, blockStatus.progress);
const progressClass = blockStatus.isClose ? "close" : "";
const breakdown = calculateVaultBreakdown();
let totalLooseValue = 0;
const looseStocks = [];
for (const stock of breakdown) {
if (stock.symbol !== sym && stock.looseShares > 0) {
totalLooseValue += stock.looseValue;
looseStocks.push({
symbol: stock.symbol,
shares: stock.looseShares,
value: stock.looseValue,
price: stock.price
});
}
}
const currentCash = getMoneyFast();
const combinedFunds = currentCash + totalLooseValue;
let affordStatus = "";
let buyBtnDisabled = "";
let rebalanceBtnDisabled = "";
let canRebalance = false;
if (blockStatus.isMaxed) {
affordStatus = '<span style="color:#8bc34a;">\u2713 Maximum tier reached</span>';
buyBtnDisabled = "disabled";
rebalanceBtnDisabled = "disabled";
} else if (blockStatus.canAfford) {
affordStatus = '<span style="color:#8bc34a;">\u2713 You can afford this!</span>';
rebalanceBtnDisabled = "disabled";
} else {
const missing = blockStatus.costToComplete - currentCash;
if (combinedFunds >= blockStatus.costToComplete) {
canRebalance = true;
affordStatus = `<span style="color:#ffd54f;">Need ${formatMoney(missing)} more - Rebalance available!</span>`;
buyBtnDisabled = "disabled";
} else {
affordStatus = `<span style="color:#ef5350;">Missing ${formatMoney(missing)} (${formatMoney(blockStatus.costToComplete - combinedFunds)} after rebalance)</span>`;
buyBtnDisabled = "disabled";
rebalanceBtnDisabled = "disabled";
}
}
let rebalanceInfo = "";
if (!blockStatus.isMaxed && totalLooseValue > 0) {
rebalanceInfo = `
<div class="quick-block-rebalance-info">
<div class="qb-rebalance-header">
<span>\u{1F504} Rebalance Option</span>
<span class="qb-rebalance-total">${formatMoney(totalLooseValue)} available</span>
</div>
<div class="qb-rebalance-detail">
Sell loose shares from ${looseStocks.length} stock${looseStocks.length > 1 ? "s" : ""} to fund this block
</div>
<div class="qb-rebalance-funds">
<span>Cash: ${formatMoney(currentCash)}</span>
<span>+</span>
<span>Loose: ${formatMoney(totalLooseValue)}</span>
<span>=</span>
<span style="color:${combinedFunds >= blockStatus.costToComplete ? "#8bc34a" : "#ef5350"};">${formatMoney(combinedFunds)}</span>
</div>
</div>`;
}
const html = `
<div class="quick-block-container">
<div class="quick-block-header">
<span class="quick-block-symbol">${sym}</span>
<span class="quick-block-type">${tierType}</span>
</div>
<div class="quick-block-status">
<div class="quick-block-current">
<span class="quick-block-label">Current</span>
<span class="quick-block-value">${currentTierDisplay}</span>
</div>
<span class="quick-block-arrow">\u2192</span>
<div class="quick-block-next">
<span class="quick-block-label">Next</span>
<span class="quick-block-value ${blockStatus.isMaxed ? "maxed" : ""}">${blockStatus.isMaxed ? "Maxed" : nextTierDisplay}</span>
</div>
</div>
${!blockStatus.isMaxed ? `
<div class="quick-block-progress-section">
<div class="quick-block-progress-header">
<span>Progress to ${nextTierDisplay}</span>
<span>${blockStatus.progress.toFixed(1)}%</span>
</div>
<div class="quick-block-progress-bar">
<div class="quick-block-progress-fill ${progressClass}" style="width:${progressWidth}%;"></div>
</div>
<div class="quick-block-shares">
${blockStatus.sharesOwned.toLocaleString()} / ${blockStatus.sharesForNextTier.toLocaleString()} shares
</div>
</div>
<div class="quick-block-details">
<div class="quick-block-detail-row">
<span>Shares Needed</span>
<span>${blockStatus.sharesNeeded.toLocaleString()}</span>
</div>
<div class="quick-block-detail-row">
<span>Current Price</span>
<span>${formatMoney(blockStatus.price)}</span>
</div>
<div class="quick-block-detail-row highlight">
<span>Cost to Complete</span>
<span>${formatMoney(blockStatus.costToComplete)}</span>
</div>
<div class="quick-block-detail-row">
<span>Tier ROI</span>
<span style="color:#caa14a;">${blockStatus.roi.toFixed(2)}% APR</span>
</div>
${blockStatus.dailyYield > 0 ? `
<div class="quick-block-detail-row">
<span>Daily Income</span>
<span style="color:#8bc34a;">${formatMoney(Math.floor(blockStatus.dailyYield))}/day</span>
</div>
` : ""}
</div>
${rebalanceInfo}
<div class="quick-block-afford">
${affordStatus}
</div>
` : ""}
<div class="quick-block-actions">
${!blockStatus.isMaxed ? `
<button id="quick-block-buy" class="alfa-main-btn qb-btn-buy" ${buyBtnDisabled} data-sym="${sym}" data-shares="${blockStatus.sharesNeeded}">
\u{1F4B5} Buy with Cash
</button>
<button id="quick-block-rebalance" class="alfa-main-btn qb-btn-rebalance" ${rebalanceBtnDisabled} data-sym="${sym}" data-shares="${blockStatus.sharesNeeded}" data-cost="${blockStatus.costToComplete}">
\u{1F504} Rebalance
</button>
` : ""}
<button id="quick-block-close" class="alfa-main-btn qb-btn-close">Close</button>
</div>
</div>`;
createModal(`Complete Block: ${sym}`, html);
$("#quick-block-chart-btn").on("click", function() {
const expandEl = document.getElementById("quick-block-chart-expand");
const container = document.getElementById("quick-block-chart-container");
const btn = this;
if (!expandEl || !container) return;
if (expandEl.style.display === "none") {
expandEl.style.display = "block";
btn.textContent = "Hide Chart";
container.innerHTML = "";
const interval = document.getElementById("quick-block-chart-interval")?.value || "d1";
renderQuickBlockChart(sym, container, interval);
} else {
expandEl.style.display = "none";
btn.textContent = "Chart";
}
});
$("#quick-block-chart-interval").on("change", function() {
const container = document.getElementById("quick-block-chart-container");
const expandEl = document.getElementById("quick-block-chart-expand");
if (container && expandEl && expandEl.style.display !== "none") {
const interval = $(this).val();
container.innerHTML = "";
renderQuickBlockChart(sym, container, interval);
}
});
$("#alfa-modal-close").off("click").on("click", function() {
$("#alfa-modal-overlay").remove();
});
$("#alfa-modal-overlay").off("click").on("click", function(e) {
if (e.target.id === "alfa-modal-overlay") {
$("#alfa-modal-overlay").remove();
}
});
$("#quick-block-buy").on("click", async function() {
const sym2 = $(this).data("sym");
const shares = parseInt($(this).data("shares"));
const $btn = $(this);
$btn.prop("disabled", true).text("Buying...");
$("#quick-block-rebalance").prop("disabled", true);
try {
await postTradeAsync(sym2, shares, "buyShares");
$btn.text("Success!").css("background", "#8bc34a").css("color", "#111");
setTimeout(() => {
$("#alfa-modal-overlay").remove();
updateVaultDisplay();
}, 1e3);
} catch (e) {
$btn.text("Error!").css("color", "#ef5350");
console.error("Quick block buy error:", e);
}
});
$("#quick-block-rebalance").on("click", function() {
const targetSym = $(this).data("sym");
const sharesNeeded = parseInt($(this).data("shares"));
const totalCost = parseFloat($(this).data("cost"));
$("#alfa-modal-overlay").remove();
const currentCash2 = getMoneyFast();
let amountNeeded = totalCost - currentCash2;
if (amountNeeded <= 0) {
alert("You already have enough cash! Use 'Buy with Cash' instead.");
return;
}
const sellFeeMultiplier = 1.001;
const safetyBuffer = 1.01;
const adjustedAmountNeeded = amountNeeded * sellFeeMultiplier * safetyBuffer;
const breakdown2 = calculateVaultBreakdown();
const sellOrders = [];
let remainingToSell = adjustedAmountNeeded;
for (const stock of breakdown2) {
if (remainingToSell <= 0) break;
if (stock.symbol === targetSym || stock.looseShares <= 0) continue;
const maxSellValue = Math.min(stock.looseValue, remainingToSell);
const sharesToSell = Math.ceil(maxSellValue / stock.price);
const actualSell = Math.min(sharesToSell, stock.looseShares);
if (actualSell > 0) {
const grossValue = actualSell * stock.price;
sellOrders.push({
symbol: stock.symbol,
shares: actualSell,
value: grossValue
});
remainingToSell -= grossValue;
}
}
if (sellOrders.length === 0) {
alert("No loose shares available to sell for rebalancing.");
return;
}
const tradeStates = {
sells: sellOrders.map(() => "pending"),
buy: "pending"
};
let completedCount = 0;
const totalTrades = sellOrders.length + 1;
const tradeQueue = [];
sellOrders.forEach((order, idx) => {
tradeQueue.push({ type: "sell", symbol: order.symbol, shares: order.shares, index: idx });
});
tradeQueue.push({ type: "buy", symbol: targetSym, shares: sharesNeeded, index: -1 });
let currentTradeIndex = 0;
function updateProgress() {
const sellsCompleted = tradeStates.sells.filter((s) => s === "completed").length;
const buyCompleted = tradeStates.buy === "completed" ? 1 : 0;
completedCount = sellsCompleted + buyCompleted;
const progressText = `Progress: ${completedCount}/${totalTrades} trades completed`;
$("#qb-rebal-progress").text(progressText);
}
function updateRowState(index, type, state) {
if (type === "sell") {
tradeStates.sells[index] = state;
const $row = $(`.rebal-row[data-qb-sell-index="${index}"]`);
if (state === "pending") {
$row.removeClass("rebal-row-executing rebal-row-completed rebal-row-failed");
} else if (state === "executing") {
$row.addClass("rebal-row-executing");
} else if (state === "completed") {
$row.removeClass("rebal-row-executing").addClass("rebal-row-completed");
} else if (state === "failed") {
$row.removeClass("rebal-row-executing").addClass("rebal-row-failed");
}
} else if (type === "buy") {
tradeStates.buy = state;
const $row = $(".rebal-section:has(.rebal-section-header:contains('Buy Order')) .rebal-row");
if (state === "pending") {
$row.removeClass("rebal-row-executing rebal-row-completed rebal-row-failed");
} else if (state === "executing") {
$row.addClass("rebal-row-executing");
} else if (state === "completed") {
$row.removeClass("rebal-row-executing").addClass("rebal-row-completed");
} else if (state === "failed") {
$row.removeClass("rebal-row-executing").addClass("rebal-row-failed");
}
}
updateProgress();
}
const sellsHtml = `
<div class="rebal-section">
<div class="rebal-section-header" style="color:#ef5350;">\u{1F4C9} Sell Orders (${sellOrders.length}) - Freeing up funds</div>
<div class="rebal-table">
${sellOrders.map((order, idx) => {
return `<div class="rebal-row" data-qb-sell-index="${idx}">
<span class="rebal-sym">${order.symbol}</span>
<span class="rebal-signal" style="color:#ef5350;">SELL</span>
<span class="rebal-reason"></span>
<span class="rebal-shares">-${order.shares.toLocaleString()}</span>
<span class="rebal-value" style="color:#ef5350;">$${formatCompactNumber(order.value)}</span>
</div>`;
}).join("")}
</div>
</div>`;
const html2 = `
<div class="rebal-container">
<div class="rebal-summary">
<div class="rebal-summary-item">
<span class="rebal-summary-label">Target</span>
<span class="rebal-summary-value">${targetSym}</span>
</div>
<div class="rebal-summary-item">
<span class="rebal-summary-label">Shares Needed</span>
<span class="rebal-summary-value">${sharesNeeded.toLocaleString()}</span>
</div>
<div class="rebal-summary-item">
<span class="rebal-summary-label">Total Cost</span>
<span class="rebal-summary-value" style="color:#caa14a;">$${formatCompactNumber(totalCost)}</span>
</div>
</div>
${sellsHtml}
<div class="rebal-section">
<div class="rebal-section-header" style="color:#66bb6a;">\u{1F4C8} Buy Order</div>
<div class="rebal-table">
<div class="rebal-row">
<span class="rebal-sym">${targetSym}</span>
<span class="rebal-signal" style="color:#66bb6a;">BUY</span>
<span class="rebal-reason"></span>
<span class="rebal-shares">+${sharesNeeded.toLocaleString()}</span>
<span class="rebal-value" style="color:#66bb6a;">$${formatCompactNumber(totalCost)}</span>
</div>
</div>
</div>
<div class="rebal-warning">
\u26A0\uFE0F Click the button to execute trades sequentially. Market prices may vary slightly.
</div>
<div id="qb-rebal-progress" style="text-align:center; padding:10px; color:#ffd54f; font-weight:bold;">Progress: 0/${totalTrades} trades completed</div>
<div class="rebal-actions">
<button id="qb-rebal-cycle-btn" class="alfa-main-btn" style="margin-right:10px;">${tradeQueue.length > 0 ? (tradeQueue[0].type === "sell" ? "Selling" : "Buying") + " " + tradeQueue[0].symbol + " (1/" + tradeQueue.length + ")" : "No trades"}</button>
<button id="qb-rebal-cancel" class="alfa-main-btn" style="border-color:#888; color:#888;">Close</button>
</div>
</div>`;
createModal(`Rebalance to Buy ${targetSym}`, html2);
$("#qb-rebal-cancel").on("click", function() {
$("#alfa-modal-overlay").remove();
});
$("#qb-rebal-cycle-btn").on("click", async function() {
if (currentTradeIndex >= tradeQueue.length) return;
const trade = tradeQueue[currentTradeIndex];
const $btn = $(this);
$btn.prop("disabled", true).text("Executing...");
updateRowState(trade.index, trade.type, "executing");
try {
await postTradeAsync(
trade.symbol,
trade.shares,
trade.type === "sell" ? "sellShares" : "buyShares"
);
updateRowState(trade.index, trade.type, "completed");
updateVaultDisplay();
currentTradeIndex++;
if (currentTradeIndex >= tradeQueue.length) {
$btn.prop("disabled", true).text("Completed!").css("background", "#8bc34a").css("color", "#111");
setTimeout(() => {
$("#alfa-modal-overlay").remove();
}, 1500);
} else {
const nextTrade = tradeQueue[currentTradeIndex];
$btn.prop("disabled", false).text(`${nextTrade.type === "sell" ? "Selling" : "Buying"} ${nextTrade.symbol} (${currentTradeIndex + 1}/${tradeQueue.length})`);
}
} catch (e) {
console.error(`Trade failed for ${trade.symbol}:`, e);
updateRowState(trade.index, trade.type, "failed");
$btn.prop("disabled", false).text(`${trade.type === "sell" ? "Selling" : "Buying"} ${trade.symbol} (${currentTradeIndex + 1}/${tradeQueue.length}) - Retry`);
}
});
});
$("#quick-block-close").on("click", function() {
$("#alfa-modal-overlay").remove();
});
}
async function ensureItemPrices(force = false) {
const key = localStorage.getItem("alfa_vault_apikey");
if (!key) return;
const last = Number(localStorage.getItem("alfa_advisor_prices_ts") || 0);
if (!force && Date.now() - last < 6 * 60 * 60 * 1e3 && Object.keys(itemPrices).length > 3) {
return;
}
const idSet = /* @__PURE__ */ new Set();
for (const b of Object.values(ADVISOR_DATA)) {
if (b.type === "item" && b.id) idSet.add(b.id);
if (b.type === "average" && Array.isArray(b.ids)) b.ids.forEach((id) => idSet.add(id));
}
if (idSet.size === 0) return;
try {
const res = await fetch(`https://api.torn.com/v2/torn/items?ids=${[...idSet].join(",")}&key=${key}`);
const data = await res.json();
const items = data.items || data.item || data;
const list = Array.isArray(items) ? items : Object.values(items || {});
let updated = 0;
for (const item of list) {
if (!item || item.id == null) continue;
const value = item.averageprice || item.market_value || item.value || item.sell_price || 0;
if (value > 0) {
itemPrices[item.id] = value;
updated++;
}
}
if (updated > 0) {
localStorage.setItem("alfa_advisor_prices", JSON.stringify(itemPrices));
localStorage.setItem("alfa_advisor_prices_ts", String(Date.now()));
}
} catch (e) {
console.error("Item price fetch error:", e);
}
}
async function loadDailyIncomeDisplay() {
if ($("#vault-daily-income").length === 0) return;
const savedKey = localStorage.getItem("alfa_vault_apikey");
if (!savedKey) {
$("#vault-daily-income").text("--");
$("#vault-bank-info").text("\u{1F3E6} Set API key in Settings");
return;
}
try {
await ensureItemPrices(false);
const incomeData = await calculateDailyIncome();
$("#vault-daily-income").text(formatMoney(Math.floor(incomeData.totalIncome)) + "/day");
if (incomeData.bankPrincipal > 0) {
$("#vault-bank-info").html(
`\u{1F3E6} ${formatCompactNumber(incomeData.bankPrincipal)} invested @ ${incomeData.bankRate.toFixed(1)}%`
);
} else {
$("#vault-bank-info").text("\u{1F3E6} No bank investment");
}
} catch (e) {
console.error("Income calculation error:", e);
$("#vault-daily-income").text("Error");
$("#vault-bank-info").text("");
}
}
let vaultDisplayTimer = null;
function updateVaultDisplay(immediate = false) {
const run = () => {
vaultDisplayTimer = null;
renderVaultSection();
};
if (immediate) {
if (vaultDisplayTimer) {
clearTimeout(vaultDisplayTimer);
vaultDisplayTimer = null;
}
run();
return;
}
if (vaultDisplayTimer) clearTimeout(vaultDisplayTimer);
vaultDisplayTimer = setTimeout(run, 150);
}
async function vaultSpread() {
const status = $("#vault-status");
if (vaultConfig.stocks.length === 0) {
status.html('<span style="color:#ef5350;">No vault stocks configured! Click Settings to set up your vault.</span>');
setTimeout(() => status.html(""), 3e3);
return;
}
let money = $("#alfa-use-api").is(":checked") ? await syncWallet(false) : getMoneyFast();
if (money === 0 && !$("#alfa-use-api").is(":checked")) money = await syncWallet(false);
if (money <= 0) {
status.html('<span style="color:#ef5350;">No cash available!</span>');
setTimeout(() => status.html(""), 3e3);
return;
}
let keepAmount = parseTornNumber($("#vault-spread-keep").val()) || vaultConfig.keepAmount || 0;
let availableCash = money - keepAmount;
if (availableCash <= 0) {
status.html('<span style="color:#ef5350;">Not enough cash after keeping amount!</span>');
setTimeout(() => status.html(""), 3e3);
return;
}
status.html('<span style="color:#ffd54f;">Calculating allocations...</span>');
if (!isGmXhrAvailable()) {
warnTornsyUnavailable();
}
const allocations = await calculateSpreadAllocations(availableCash, vaultConfig.depositStrategy);
if (allocations.length === 0) {
status.html('<span style="color:#ef5350;">Could not calculate allocations!</span>');
setTimeout(() => status.html(""), 3e3);
return;
}
status.html("");
showSpreadModal(allocations, availableCash, keepAmount);
}
async function calculateSpreadAllocations(totalCash, strategy) {
let stocks2 = vaultConfig.stocks;
if (stocks2.length === 0) return [];
if (vaultConfig.lockedStocks && vaultConfig.lockedStocks.length > 0) {
stocks2 = stocks2.filter((sym) => !vaultConfig.lockedStocks.includes(sym));
}
if (vaultConfig.skipOvervalued || $("#vault-skip-overvalued").is(":checked")) {
const overvaluedSignals = strategy === "swing_strategy" ? ["strong_sell", "sell", "take_profit"] : ["sell", "take_profit"];
stocks2 = stocks2.filter((sym) => {
const analysis = vaultAnalysisCache[sym];
if (!analysis) return true;
if (strategy === "swing_strategy") {
const opportunity = analyzeSwingOpportunity(analysis);
return opportunity ? !overvaluedSignals.includes(opportunity.signal) : true;
}
return !overvaluedSignals.includes(analysis.signal);
});
}
if (stocks2.length === 0) {
$("#vault-status").html('<span style="color:#ffb74d;">No eligible stocks! Check locks or run analysis.</span>');
return [];
}
let allocations = [];
switch (strategy) {
case "swing_strategy":
allocations = await swingStrategyAllocation(totalCash, stocks2);
break;
case "dip_weighted":
allocations = await dipWeightedAllocation(totalCash, stocks2);
break;
case "rsi_weighted":
allocations = await rsiWeightedAllocation(totalCash, stocks2);
break;
case "roi_priority":
allocations = await roiPriorityAllocation(totalCash, stocks2);
break;
case "equal":
default:
allocations = await equalSplitAllocation(totalCash, stocks2);
break;
}
allocations = adjustForBenefitTiers(allocations);
return allocations.filter((a) => a.shares > 0);
}
async function swingStrategyAllocation(totalCash, stocks2) {
const allocations = [];
let weights = {};
let totalWeight = 0;
for (const sym of stocks2) {
let analysis = vaultAnalysisCache[sym];
if (!analysis) {
analysis = await analyzeStockForVault(sym);
}
const opportunity = analysis ? analyzeSwingOpportunity(analysis) : null;
let weight = 1;
if (opportunity) {
weight = Math.max(0.5, 1 + opportunity.score / 100);
}
weights[sym] = weight;
totalWeight += weight;
}
for (const sym of stocks2) {
const price = getPrice(sym);
if (price <= 0 || totalWeight <= 0) continue;
const allocation = weights[sym] / totalWeight * totalCash;
const shares = Math.floor(allocation / price);
if (shares > 0) {
allocations.push({
symbol: sym,
shares,
cost: shares * price,
price,
weight: weights[sym]
});
}
}
return redistributeRemainder(allocations, totalCash, weights, totalWeight);
}
async function equalSplitAllocation(totalCash, stocks2) {
const allocations = [];
let weights = {};
let totalWeight = 0;
for (const sym of stocks2) {
let weight = 1;
let analysis = vaultAnalysisCache[sym];
if (!analysis) analysis = await analyzeStockForVault(sym);
if (analysis) {
const signal = analysis.signal;
if (signal === "buy") weight *= 1.3;
else if (signal === "good") weight *= 1.15;
const volMult = analysis.volumeMultiplier ?? 1;
const volMin = swingTradeData?.settings?.volumeMinMultiplier ?? 1;
const volPartial = swingTradeData?.settings?.volumePartialMultiplier ?? 0.8;
const volFactor = volMult >= volMin ? 1 : volMult >= volPartial ? 0.5 : 0;
weight = weight * volFactor + (1 - volFactor);
}
weights[sym] = weight;
totalWeight += weight;
}
for (const sym of stocks2) {
const price = getPrice(sym);
if (price <= 0 || totalWeight <= 0) continue;
const allocation = weights[sym] / totalWeight * totalCash;
const shares = Math.floor(allocation / price);
if (shares > 0) {
allocations.push({
symbol: sym,
shares,
cost: shares * price,
price,
weight: weights[sym]
});
}
}
return redistributeRemainder(allocations, totalCash, weights, totalWeight);
}
function redistributeRemainder(allocations, totalCash, weights, totalWeight) {
if (allocations.length === 0) return allocations;
let totalSpent = allocations.reduce((sum, a) => sum + a.cost, 0);
let remainder = totalCash - totalSpent;
const sorted = [...allocations].sort((a, b) => (b.weight || 1) - (a.weight || 1));
let iterations = 0;
const maxIterations = allocations.length * 3;
while (remainder > 0 && iterations < maxIterations) {
let addedAny = false;
for (const alloc of sorted) {
if (alloc.price <= remainder) {
alloc.shares += 1;
alloc.cost = alloc.shares * alloc.price;
remainder -= alloc.price;
addedAny = true;
if (remainder <= 0) break;
}
}
if (!addedAny) break;
iterations++;
}
return allocations;
}
async function dipWeightedAllocation(totalCash, stocks2) {
const allocations = [];
let weights = {};
let totalWeight = 0;
for (const sym of stocks2) {
let analysis = vaultAnalysisCache[sym];
if (!analysis) {
analysis = await analyzeStockForVault(sym);
}
let weight = 1;
if (analysis && analysis.dipFrom7d < 0) {
weight = Math.abs(analysis.dipFrom7d) + 1;
} else if (analysis && analysis.dipFrom7d > 3) {
weight = 0.5;
}
if (analysis) {
const volMult = analysis.volumeMultiplier ?? 1;
const volMin = swingTradeData?.settings?.volumeMinMultiplier ?? 1;
const volPartial = swingTradeData?.settings?.volumePartialMultiplier ?? 0.8;
const volFactor = volMult >= volMin ? 1 : volMult >= volPartial ? 0.5 : 0;
weight = weight * volFactor + (1 - volFactor);
}
if (analysis) {
const signal = analysis.signal;
if (signal === "buy") weight *= 1.3;
else if (signal === "good") weight *= 1.15;
}
weights[sym] = weight;
totalWeight += weight;
}
for (const sym of stocks2) {
const price = getPrice(sym);
if (price <= 0 || totalWeight <= 0) continue;
const allocation = weights[sym] / totalWeight * totalCash;
const shares = Math.floor(allocation / price);
if (shares > 0) {
allocations.push({
symbol: sym,
shares,
cost: shares * price,
price,
weight: weights[sym]
});
}
}
return redistributeRemainder(allocations, totalCash, weights, totalWeight);
}
async function rsiWeightedAllocation(totalCash, stocks2) {
const allocations = [];
let weights = {};
let totalWeight = 0;
for (const sym of stocks2) {
let analysis = vaultAnalysisCache[sym];
if (!analysis) {
analysis = await analyzeStockForVault(sym);
}
let weight = 1;
if (analysis && analysis.rsi) {
weight = Math.max(0.5, (100 - analysis.rsi) / 30);
const adx = analysis.adx ?? 25;
const adxMin = swingTradeData?.settings?.adxMinThreshold ?? 25;
const adxPartial = swingTradeData?.settings?.adxPartialThreshold ?? 20;
const rsiAdxFactor = adx >= adxMin ? 1 : adx >= adxPartial ? 0.5 : 0;
weight = weight * rsiAdxFactor + (1 - rsiAdxFactor);
}
if (analysis) {
const volMult = analysis.volumeMultiplier ?? 1;
const volMin = swingTradeData?.settings?.volumeMinMultiplier ?? 1;
const volPartial = swingTradeData?.settings?.volumePartialMultiplier ?? 0.8;
const volFactor = volMult >= volMin ? 1 : volMult >= volPartial ? 0.5 : 0;
weight = weight * volFactor + (1 - volFactor);
}
if (analysis) {
const signal = analysis.signal;
if (signal === "buy") weight *= 1.3;
else if (signal === "good") weight *= 1.15;
}
weights[sym] = weight;
totalWeight += weight;
}
for (const sym of stocks2) {
const price = getPrice(sym);
if (price <= 0 || totalWeight <= 0) continue;
const allocation = weights[sym] / totalWeight * totalCash;
const shares = Math.floor(allocation / price);
if (shares > 0) {
allocations.push({
symbol: sym,
shares,
cost: shares * price,
price,
weight: weights[sym]
});
}
}
return redistributeRemainder(allocations, totalCash, weights, totalWeight);
}
async function roiPriorityAllocation(totalCash, stocks2) {
const allocations = [];
let weights = {};
let totalWeight = 0;
for (const sym of stocks2) {
const price = getPrice(sym);
const dailyYield = getDailyYield(sym);
const stockData = STOCK_DATA[sym];
if (!stockData || price <= 0) continue;
const tierCost = stockData.base * price;
const roi = tierCost > 0 ? dailyYield * 365 / tierCost * 100 : 0;
let weight = Math.max(0.5, roi / 10);
if (isGmXhrAvailable()) {
let analysis = vaultAnalysisCache[sym];
if (!analysis) analysis = await analyzeStockForVault(sym);
if (analysis) {
const volMult = analysis.volumeMultiplier ?? 1;
const volMin = swingTradeData?.settings?.volumeMinMultiplier ?? 1;
const volPartial = swingTradeData?.settings?.volumePartialMultiplier ?? 0.8;
const volFactor = volMult >= volMin ? 1 : volMult >= volPartial ? 0.5 : 0;
weight = weight * volFactor + (1 - volFactor);
const signal = analysis.signal;
if (signal === "buy") weight *= 1.3;
else if (signal === "good") weight *= 1.15;
}
}
weights[sym] = weight;
totalWeight += weight;
}
for (const sym of stocks2) {
const price = getPrice(sym);
if (price <= 0 || totalWeight <= 0) continue;
const allocation = weights[sym] / totalWeight * totalCash;
const shares = Math.floor(allocation / price);
if (shares > 0) {
allocations.push({
symbol: sym,
shares,
cost: shares * price,
price,
weight: weights[sym]
});
}
}
return redistributeRemainder(allocations, totalCash, weights, totalWeight);
}
function adjustForBenefitTiers(allocations) {
return allocations.map((alloc) => {
const stockData = STOCK_DATA[alloc.symbol];
if (!stockData) return alloc;
const currentOwned = getOwnedShares(alloc.symbol);
const totalAfter = currentOwned + alloc.shares;
if (stockData.type === "P") {
const tierTarget = stockData.base;
if (currentOwned < tierTarget && totalAfter >= tierTarget * 0.9 && totalAfter < tierTarget) {
const sharesToTier = tierTarget - currentOwned;
const costToTier = sharesToTier * alloc.price;
if (costToTier <= alloc.cost * 1.2) {
alloc.shares = sharesToTier;
alloc.cost = costToTier;
}
}
} else {
const base = stockData.base;
const currentTier = getBenefitTier(alloc.symbol, currentOwned);
const nextTierShares = currentTier.next || base;
if (totalAfter >= nextTierShares * 0.9 && totalAfter < nextTierShares) {
const sharesToTier = nextTierShares - currentOwned;
const costToTier = sharesToTier * alloc.price;
if (costToTier <= alloc.cost * 1.2) {
alloc.shares = sharesToTier;
alloc.cost = costToTier;
}
}
}
return alloc;
});
}
function showSpreadModal(allocations, availableCash, keepAmount) {
const totalAllocated = allocations.reduce((sum, a) => sum + a.cost, 0);
const strategyLabel = vaultConfig.depositStrategy.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase());
const tradeStates = allocations.map(() => "pending");
let completedCount = 0;
const tradeQueue = allocations.map((alloc, idx) => ({
type: "buy",
symbol: alloc.symbol,
shares: alloc.shares,
index: idx
}));
let currentTradeIndex = 0;
function updateProgress() {
completedCount = tradeStates.filter((s) => s === "completed").length;
const progressText = `Progress: ${completedCount}/${allocations.length} trades completed`;
$("#spread-progress").text(progressText);
}
function updateRowState(index, state) {
const $row = $(`.rebal-row[data-spread-index="${index}"]`);
tradeStates[index] = state;
if (state === "pending") {
$row.removeClass("rebal-row-executing rebal-row-completed rebal-row-failed");
} else if (state === "executing") {
$row.addClass("rebal-row-executing");
} else if (state === "completed") {
$row.removeClass("rebal-row-executing").addClass("rebal-row-completed");
} else if (state === "failed") {
$row.removeClass("rebal-row-executing").addClass("rebal-row-failed");
}
updateProgress();
}
const allocationsHtml = `
<div class="rebal-section">
<div class="rebal-section-header" style="color:#66bb6a;">\u{1F4C8} Buy Orders (${allocations.length})</div>
<div class="rebal-table">
${allocations.map((alloc, idx) => {
return `<div class="rebal-row" data-spread-index="${idx}">
<span class="rebal-sym">${alloc.symbol}</span>
<span class="rebal-signal" style="color:#66bb6a;">BUY</span>
<span class="rebal-reason"></span>
<span class="rebal-shares">+${alloc.shares.toLocaleString()}</span>
<span class="rebal-value" style="color:#66bb6a;">$${formatCompactNumber(alloc.cost)}</span>
</div>`;
}).join("")}
</div>
<div class="rebal-subtotal" style="color:#66bb6a;">Total: $${formatCompactNumber(totalAllocated)}</div>
</div>`;
const html = `
<div class="rebal-container">
<div class="rebal-summary">
<div class="rebal-summary-item">
<span class="rebal-summary-label">Strategy</span>
<span class="rebal-summary-value">${strategyLabel}</span>
</div>
<div class="rebal-summary-item">
<span class="rebal-summary-label">Amount to Spread</span>
<span class="rebal-summary-value" style="color:#66bb6a;">$${formatCompactNumber(availableCash)}</span>
</div>
<div class="rebal-summary-item">
<span class="rebal-summary-label">Keeping</span>
<span class="rebal-summary-value">$${formatCompactNumber(keepAmount)}</span>
</div>
</div>
${allocationsHtml}
<div class="rebal-warning">
\u26A0\uFE0F Click the button to execute trades sequentially. Market prices may vary slightly.
</div>
<div id="spread-progress" style="text-align:center; padding:10px; color:#66bb6a; font-weight:bold;">Progress: 0/${allocations.length} trades completed</div>
<div class="rebal-actions">
<button id="spread-cycle-btn" class="alfa-main-btn" style="margin-right:10px;">${tradeQueue.length > 0 ? `Buying ${tradeQueue[0].symbol} (1/${tradeQueue.length})` : "No trades"}</button>
<button id="spread-cancel" class="alfa-main-btn" style="border-color:#888; color:#888;">Close</button>
</div>
</div>`;
createModal("Vault Spread", html);
$("#spread-cancel").on("click", function() {
$("#alfa-modal-overlay").remove();
});
$("#spread-cycle-btn").on("click", async function() {
if (currentTradeIndex >= tradeQueue.length) return;
const trade = tradeQueue[currentTradeIndex];
const $btn = $(this);
$btn.prop("disabled", true).text("Executing...");
updateRowState(trade.index, "executing");
try {
await postTradeAsync(trade.symbol, trade.shares, "buyShares");
updateRowState(trade.index, "completed");
updateVaultDisplay();
currentTradeIndex++;
if (currentTradeIndex >= tradeQueue.length) {
$btn.prop("disabled", true).text("Completed!").css("background", "#8bc34a").css("color", "#111");
} else {
const nextTrade = tradeQueue[currentTradeIndex];
$btn.prop("disabled", false).text(`Buying ${nextTrade.symbol} (${currentTradeIndex + 1}/${tradeQueue.length})`);
}
} catch (e) {
console.error(`Buy failed for ${trade.symbol}:`, e);
updateRowState(trade.index, "failed");
$btn.prop("disabled", false).text(`Buying ${trade.symbol} (${currentTradeIndex + 1}/${tradeQueue.length}) - Retry`);
}
});
}
async function executeSpreadTrades(allocations) {
const status = $("#vault-status");
let successCount = 0;
let failCount = 0;
for (let i = 0; i < allocations.length; i++) {
const alloc = allocations[i];
status.html(`<span style="color:#ffd54f;">Buying ${alloc.symbol}... (${i + 1}/${allocations.length})</span>`);
try {
await new Promise((resolve, reject) => {
$.post(
`https://www.torn.com/page.php?sid=StockMarket&step=buyShares&rfcv=${getRFC()}`,
{ stockId: stockId[alloc.symbol], amount: alloc.shares }
).done(function(r) {
try {
if (typeof r === "string") r = JSON.parse(r);
if (r.success) {
updateLocalCache(alloc.symbol, alloc.shares);
successCount++;
resolve();
} else {
failCount++;
console.error(`Trade failed for ${alloc.symbol}:`, r.text);
resolve();
}
} catch (e) {
successCount++;
resolve();
}
}).fail(function() {
failCount++;
resolve();
});
});
if (i < allocations.length - 1) {
await new Promise((r) => setTimeout(r, 300));
}
} catch (e) {
failCount++;
console.error(`Error trading ${alloc.symbol}:`, e);
}
}
updateVaultDisplay();
if (failCount === 0) {
status.html(`<span style="color:#8bc34a;">Vault spread complete! ${successCount} trades executed.</span>`);
} else {
status.html(`<span style="color:#ffb74d;">Completed with ${failCount} failed trades. ${successCount} succeeded.</span>`);
}
setTimeout(() => status.html(""), 5e3);
}
async function vaultAnalyze() {
const status = $("#vault-status");
if (vaultConfig.stocks.length === 0) {
status.html('<span style="color:#ef5350;">No vault stocks configured!</span>');
setTimeout(() => status.html(""), 3e3);
return;
}
status.html('<span style="color:#ffd54f;">Analyzing stocks...</span>');
await analyzeAllVaultStocks((msg) => status.html(`<span style="color:#ffd54f;">${msg}</span>`));
renderVaultSection();
$("#vault-status").html('<span style="color:#8bc34a;">Analysis complete! Signals updated.</span>');
setTimeout(() => $("#vault-status").html(""), 3e3);
}
async function vaultRebalance() {
const status = $("#vault-status");
if (vaultConfig.stocks.length === 0) {
alert("No vault stocks configured! Click Config to set up your vault.");
return;
}
if (Object.keys(vaultAnalysisCache).length === 0) {
if (isGmXhrAvailable()) {
status.html('<span style="color:#ffd54f;">Running analysis first...</span>');
await analyzeAllVaultStocks((msg) => status.html(`<span style="color:#ffd54f;">${msg}</span>`));
} else {
warnTornsyUnavailable();
}
}
status.html('<span style="color:#ffd54f;">Calculating rebalance plan...</span>');
const breakdown = calculateVaultBreakdown();
const totalVaultValue = breakdown.reduce((sum, s) => sum + s.value, 0);
if (totalVaultValue <= 0) {
status.html('<span style="color:#ef5350;">No vault holdings to rebalance!</span>');
return;
}
let eligibleStocks = vaultConfig.stocks.filter(
(sym) => !vaultConfig.lockedStocks || !vaultConfig.lockedStocks.includes(sym)
);
if (eligibleStocks.length === 0) {
status.html('<span style="color:#ef5350;">All stocks are locked!</span>');
return;
}
const rebalancePlan = await calculateSmartRebalancePlan(breakdown, eligibleStocks, totalVaultValue);
if (rebalancePlan.sells.length === 0 && rebalancePlan.buys.length === 0) {
status.html('<span style="color:#8bc34a;">Portfolio is already balanced!</span>');
setTimeout(() => status.html(""), 3e3);
return;
}
showRebalanceModal(rebalancePlan, breakdown, null);
}
async function calculateSmartRebalancePlan(currentBreakdown, eligibleStocks, totalVaultValue) {
const sells = [];
const buys = [];
const lockBlocks = $("#alfa-lock-toggle").is(":checked");
const smartMode = vaultConfig.skipOvervalued || $("#vault-skip-overvalued").is(":checked");
const overvaluedSignals = ["sell", "take_profit"];
const buySignals = ["buy", "good", "hold"];
const currentMap = {};
for (const stock of currentBreakdown) {
if (eligibleStocks.includes(stock.symbol)) {
currentMap[stock.symbol] = stock;
}
}
const idealAllocations = await calculateIdealRebalanceAllocations(totalVaultValue, eligibleStocks, smartMode);
const idealMap = {};
for (const alloc of idealAllocations) {
idealMap[alloc.symbol] = alloc;
}
let totalSellValue = 0;
let totalBuyValue = 0;
for (const sym of eligibleStocks) {
const current = currentMap[sym];
const ideal = idealMap[sym];
const currentValue = current ? current.value : 0;
const currentShares = current ? current.owned : 0;
const idealValue = ideal ? ideal.cost : 0;
const idealShares = ideal ? ideal.shares : 0;
const price = current ? current.price : ideal ? ideal.price : getPrice(sym);
if (price <= 0) continue;
const valueDiff = idealValue - currentValue;
const analysis = vaultAnalysisCache[sym];
const signal = analysis ? analysis.signal : "hold";
const threshold = Math.max(Math.max(currentValue, idealValue) * 0.02, 5e3);
if (Math.abs(valueDiff) < threshold) continue;
if (valueDiff < 0) {
const excessValue = Math.abs(valueDiff);
let sharesToSell = Math.floor(excessValue / price);
if (lockBlocks && current) {
sharesToSell = Math.min(sharesToSell, current.looseShares);
} else if (current) {
sharesToSell = Math.min(sharesToSell, current.owned);
}
if (sharesToSell > 0) {
const sellValue = sharesToSell * price;
let reason = "Over target allocation";
if (smartMode && overvaluedSignals.includes(signal)) {
reason = `${signal.toUpperCase()} signal`;
}
sells.push({
symbol: sym,
shares: sharesToSell,
value: sellValue,
price,
currentShares,
idealShares,
analysis,
reason
});
totalSellValue += sellValue;
}
} else if (valueDiff > 0) {
const shortfallValue = valueDiff;
const sharesToBuy = Math.floor(shortfallValue / price);
if (sharesToBuy > 0) {
buys.push({
symbol: sym,
shares: sharesToBuy,
value: sharesToBuy * price,
price,
currentShares,
idealShares,
analysis
});
totalBuyValue += sharesToBuy * price;
}
}
}
if (totalBuyValue > totalSellValue && totalSellValue > 0) {
const scale = totalSellValue / totalBuyValue;
totalBuyValue = 0;
for (const buy of buys) {
buy.shares = Math.floor(buy.shares * scale);
buy.value = buy.shares * buy.price;
totalBuyValue += buy.value;
}
const filteredBuys = buys.filter((b) => b.shares > 0);
buys.length = 0;
buys.push(...filteredBuys);
let remainder = totalSellValue - totalBuyValue;
if (remainder > 0 && buys.length > 0) {
buys.sort((a, b) => {
const signalPriority2 = { "buy": 1, "good": 2, "hold": 3, "take_profit": 4, "sell": 5 };
const aPri = a.analysis ? signalPriority2[a.analysis.signal] || 3 : 3;
const bPri = b.analysis ? signalPriority2[b.analysis.signal] || 3 : 3;
return aPri - bPri;
});
for (const buy of buys) {
if (remainder < buy.price) break;
const extraShares = Math.floor(remainder / buy.price);
if (extraShares > 0) {
const addShares = Math.min(extraShares, Math.floor(remainder / buy.price));
buy.shares += addShares;
buy.value = buy.shares * buy.price;
remainder -= addShares * buy.price;
totalBuyValue += addShares * buy.price;
}
}
}
}
const signalPriority = { "sell": 1, "take_profit": 2, "hold": 3, "good": 4, "buy": 5 };
sells.sort((a, b) => {
const aPri = a.analysis ? signalPriority[a.analysis.signal] || 3 : 3;
const bPri = b.analysis ? signalPriority[b.analysis.signal] || 3 : 3;
return aPri - bPri;
});
buys.sort((a, b) => {
const aPri = a.analysis ? signalPriority[a.analysis.signal] || 3 : 3;
const bPri = b.analysis ? signalPriority[b.analysis.signal] || 3 : 3;
return bPri - aPri;
});
return {
sells,
buys,
totalSellValue,
totalBuyValue,
netCashFlow: totalSellValue - totalBuyValue
};
}
async function calculateIdealRebalanceAllocations(totalValue, stocks2, smartMode) {
const strategy = vaultConfig.depositStrategy;
const overvaluedSignals = ["sell", "take_profit"];
let allocations = [];
let weights = {};
let totalWeight = 0;
for (const sym of stocks2) {
let analysis = vaultAnalysisCache[sym];
if (!analysis) {
analysis = await analyzeStockForVault(sym);
}
const signal = analysis ? analysis.signal : "hold";
const opportunity = analysis ? analyzeSwingOpportunity(analysis) : null;
const swingOvervalued = ["strong_sell", "sell", "take_profit"];
const isOvervalued = strategy === "swing_strategy" && opportunity ? swingOvervalued.includes(opportunity.signal) : overvaluedSignals.includes(signal);
if (smartMode && isOvervalued) {
weights[sym] = 0;
continue;
}
let weight = 1;
switch (strategy) {
case "swing_strategy":
if (opportunity) {
weight = Math.max(0.5, 1 + opportunity.score / 100);
}
break;
case "rsi_weighted":
if (analysis && analysis.rsi) {
weight = Math.max(0.5, (100 - analysis.rsi) / 30);
const adx = analysis.adx ?? 25;
const adxMin = swingTradeData?.settings?.adxMinThreshold ?? 25;
const adxPartial = swingTradeData?.settings?.adxPartialThreshold ?? 20;
const rsiAdxFactor = adx >= adxMin ? 1 : adx >= adxPartial ? 0.5 : 0;
weight = weight * rsiAdxFactor + (1 - rsiAdxFactor);
const volMult = analysis.volumeMultiplier ?? 1;
const volMin = swingTradeData?.settings?.volumeMinMultiplier ?? 1;
const volPartial = swingTradeData?.settings?.volumePartialMultiplier ?? 0.8;
const volFactor = volMult >= volMin ? 1 : volMult >= volPartial ? 0.5 : 0;
weight = weight * volFactor + (1 - volFactor);
}
break;
case "dip_weighted":
if (analysis && analysis.dipFrom7d < 0) {
weight = Math.abs(analysis.dipFrom7d) + 1;
} else if (analysis && analysis.dipFrom7d > 3) {
weight = 0.5;
}
if (analysis) {
const volMult = analysis.volumeMultiplier ?? 1;
const volMin = swingTradeData?.settings?.volumeMinMultiplier ?? 1;
const volPartial = swingTradeData?.settings?.volumePartialMultiplier ?? 0.8;
const volFactor = volMult >= volMin ? 1 : volMult >= volPartial ? 0.5 : 0;
weight = weight * volFactor + (1 - volFactor);
}
break;
case "roi_priority":
const price = getPrice(sym);
const dailyYield = getDailyYield(sym);
const stockData = STOCK_DATA[sym];
if (stockData && price > 0) {
const tierCost = stockData.base * price;
const roi = tierCost > 0 ? dailyYield * 365 / tierCost * 100 : 0;
weight = Math.max(0.5, roi / 10);
}
if (analysis) {
const volMult = analysis.volumeMultiplier ?? 1;
const volMin = swingTradeData?.settings?.volumeMinMultiplier ?? 1;
const volPartial = swingTradeData?.settings?.volumePartialMultiplier ?? 0.8;
const volFactor = volMult >= volMin ? 1 : volMult >= volPartial ? 0.5 : 0;
weight = weight * volFactor + (1 - volFactor);
}
break;
case "equal":
default:
weight = 1;
if (analysis) {
const volMult = analysis.volumeMultiplier ?? 1;
const volMin = swingTradeData?.settings?.volumeMinMultiplier ?? 1;
const volPartial = swingTradeData?.settings?.volumePartialMultiplier ?? 0.8;
const volFactor = volMult >= volMin ? 1 : volMult >= volPartial ? 0.5 : 0;
weight = weight * volFactor + (1 - volFactor);
}
break;
}
if (analysis) {
if (signal === "buy") weight *= 1.3;
else if (signal === "good") weight *= 1.15;
}
weights[sym] = weight;
totalWeight += weight;
}
for (const sym of stocks2) {
const price = getPrice(sym);
if (price <= 0 || totalWeight <= 0) continue;
const weight = weights[sym] || 0;
const allocation = weight / totalWeight * totalValue;
const shares = Math.floor(allocation / price);
allocations.push({
symbol: sym,
shares,
cost: shares * price,
price,
weight
});
}
return allocations;
}
function showRebalanceModal(plan, currentBreakdown, idealAllocations) {
const signalColors = {
"buy": "#66bb6a",
"good": "#81c784",
"hold": "#ffd54f",
"take_profit": "#ffb74d",
"sell": "#ef5350"
};
const tradeStates = {
sells: plan.sells.map(() => "pending"),
buys: plan.buys.map(() => "pending")
};
let completedCount = 0;
const totalTrades = plan.sells.length + plan.buys.length;
const tradeQueue = [];
plan.sells.forEach((s, idx) => {
tradeQueue.push({ type: "sell", symbol: s.symbol, shares: s.shares, index: idx });
});
plan.buys.forEach((b, idx) => {
tradeQueue.push({ type: "buy", symbol: b.symbol, shares: b.shares, index: idx });
});
let currentTradeIndex = 0;
function updateProgress() {
completedCount = tradeStates.sells.filter((s) => s === "completed").length + tradeStates.buys.filter((b) => b === "completed").length;
const progressText = totalTrades > 0 ? `Progress: ${completedCount}/${totalTrades} trades completed` : "";
$("#rebal-progress").text(progressText);
}
function updateRowState(index, type, state) {
const typeKey = type + "s";
tradeStates[typeKey][index] = state;
const $row = type === "sell" ? $(`.rebal-row[data-sell-index="${index}"]`) : $(`.rebal-row[data-buy-index="${index}"]`);
if (state === "pending") {
$row.removeClass("rebal-row-executing rebal-row-completed rebal-row-failed");
} else if (state === "executing") {
$row.addClass("rebal-row-executing");
} else if (state === "completed") {
$row.removeClass("rebal-row-executing").addClass("rebal-row-completed");
} else if (state === "failed") {
$row.removeClass("rebal-row-executing").addClass("rebal-row-failed");
}
updateProgress();
}
let sellsHtml = "";
if (plan.sells.length > 0) {
sellsHtml = `
<div class="rebal-section">
<div class="rebal-section-header" style="color:#ef5350;">\u{1F4C9} Sell Orders (${plan.sells.length}) - Freeing up funds</div>
<div class="rebal-table">
${plan.sells.map((s, idx) => {
const signal = s.analysis ? s.analysis.signal : "hold";
const signalColor = signalColors[signal] || "#888";
const reason = s.reason || "";
const reasonTip = reason.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
const reasonHtml = reason ? `<span class="rebal-reason rebal-reason-tip" title="${reasonTip}" aria-label="${reasonTip}">\u24D8</span>` : `<span class="rebal-reason"></span>`;
return `<div class="rebal-row" data-sell-index="${idx}">
<span class="rebal-sym">${s.symbol}</span>
<span class="rebal-signal" style="color:${signalColor};">${signal.toUpperCase()}</span>
${reasonHtml}
<span class="rebal-shares">-${s.shares.toLocaleString()}</span>
<span class="rebal-value" style="color:#ef5350;">${formatCompactNumber(s.value)}</span>
</div>`;
}).join("")}
</div>
<div class="rebal-subtotal" style="color:#ef5350;">Funds to redistribute: $${formatCompactNumber(plan.totalSellValue)}</div>
</div>`;
} else {
sellsHtml = `
<div class="rebal-section">
<div class="rebal-section-header" style="color:#888;">\u{1F4C9} No Sell Orders</div>
<div style="padding:10px; color:#666; font-size:11px; text-align:center;">
No overvalued or overweight positions found to sell.
${vaultConfig.skipOvervalued ? '<br>Try disabling "Smart" mode to include more stocks.' : ""}
</div>
</div>`;
}
let buysHtml = "";
if (plan.buys.length > 0) {
buysHtml = `
<div class="rebal-section">
<div class="rebal-section-header" style="color:#66bb6a;">\u{1F4C8} Buy Orders (${plan.buys.length}) - Redistributing to undervalued</div>
<div class="rebal-table">
${plan.buys.map((b, idx) => {
const signal = b.analysis ? b.analysis.signal : "hold";
const signalColor = signalColors[signal] || "#888";
return `<div class="rebal-row" data-buy-index="${idx}">
<span class="rebal-sym">${b.symbol}</span>
<span class="rebal-signal" style="color:${signalColor};">${signal.toUpperCase()}</span>
<span class="rebal-reason"></span>
<span class="rebal-shares">+${b.shares.toLocaleString()}</span>
<span class="rebal-value" style="color:#66bb6a;">$${formatCompactNumber(b.value)}</span>
</div>`;
}).join("")}
</div>
<div class="rebal-subtotal" style="color:#66bb6a;">Total Buy: $${formatCompactNumber(plan.totalBuyValue)}</div>
</div>`;
} else if (plan.sells.length > 0) {
buysHtml = `
<div class="rebal-section">
<div class="rebal-section-header" style="color:#888;">\u{1F4C8} No Buy Orders</div>
<div style="padding:10px; color:#666; font-size:11px; text-align:center;">
No eligible stocks to buy into. Freed funds will remain as cash.
</div>
</div>`;
}
const netFlow = plan.netCashFlow;
const hasAction = plan.sells.length > 0 || plan.buys.length > 0;
let summaryHtml = "";
if (plan.sells.length > 0 && plan.buys.length > 0) {
summaryHtml = `
<div class="rebal-summary">
<div class="rebal-summary-item">
<span class="rebal-summary-label">Strategy</span>
<span class="rebal-summary-value">${vaultConfig.depositStrategy.replace("_", " ")}</span>
</div>
<div class="rebal-summary-item">
<span class="rebal-summary-label">Sell \u2192 Buy</span>
<span class="rebal-summary-value" style="color:#caa14a;">$${formatCompactNumber(plan.totalSellValue)}</span>
</div>
<div class="rebal-summary-item">
<span class="rebal-summary-label">Total Trades</span>
<span class="rebal-summary-value">${totalTrades}</span>
</div>
</div>`;
} else if (plan.sells.length > 0) {
summaryHtml = `
<div class="rebal-summary">
<div class="rebal-summary-item">
<span class="rebal-summary-label">Action</span>
<span class="rebal-summary-value">Sell Only</span>
</div>
<div class="rebal-summary-item">
<span class="rebal-summary-label">Cash Out</span>
<span class="rebal-summary-value" style="color:#ef5350;">$${formatCompactNumber(plan.totalSellValue)}</span>
</div>
</div>`;
}
const html = `
<div class="rebal-container">
${summaryHtml}
${sellsHtml}
${buysHtml}
${hasAction ? `<div class="rebal-warning">
\u26A0\uFE0F Click the button to execute trades sequentially. Market prices may vary slightly.
</div>` : ""}
${hasAction ? `<div id="rebal-progress" style="text-align:center; padding:10px; color:#caa14a; font-weight:bold;">Progress: 0/${totalTrades} trades completed</div>` : ""}
<div class="rebal-actions">
${hasAction && tradeQueue.length > 0 ? `<button id="rebal-cycle-btn" class="alfa-main-btn" style="margin-right:10px;">${tradeQueue[0].type === "sell" ? "Selling" : "Buying"} ${tradeQueue[0].symbol} (1/${tradeQueue.length})</button>` : ""}
<button id="rebal-cancel" class="alfa-main-btn" style="border-color:#888; color:#888;">Close</button>
</div>
</div>`;
createModal("Rebalance Portfolio", html);
$("#rebal-cancel").on("click", function() {
$("#alfa-modal-overlay").remove();
});
if (hasAction && tradeQueue.length > 0) {
$("#rebal-cycle-btn").on("click", async function() {
if (currentTradeIndex >= tradeQueue.length) return;
const trade = tradeQueue[currentTradeIndex];
const $btn = $(this);
$btn.prop("disabled", true).text("Executing...");
updateRowState(trade.index, trade.type, "executing");
try {
await postTradeAsync(
trade.symbol,
trade.shares,
trade.type === "sell" ? "sellShares" : "buyShares"
);
updateRowState(trade.index, trade.type, "completed");
updateVaultDisplay();
currentTradeIndex++;
if (currentTradeIndex >= tradeQueue.length) {
$btn.prop("disabled", true).text("Completed!").css("background", "#8bc34a").css("color", "#111");
} else {
const nextTrade = tradeQueue[currentTradeIndex];
$btn.prop("disabled", false).text(`${nextTrade.type === "sell" ? "Selling" : "Buying"} ${nextTrade.symbol} (${currentTradeIndex + 1}/${tradeQueue.length})`);
}
} catch (e) {
console.error(`Trade failed for ${trade.symbol}:`, e);
updateRowState(trade.index, trade.type, "failed");
$btn.prop("disabled", false).text(`${trade.type === "sell" ? "Selling" : "Buying"} ${trade.symbol} (${currentTradeIndex + 1}/${tradeQueue.length}) - Retry`);
}
});
}
}
async function executeRebalance(plan) {
const status = $("#vault-status");
let successCount = 0;
let failCount = 0;
for (let i = 0; i < plan.sells.length; i++) {
const sell = plan.sells[i];
status.html(`<span style="color:#ffd54f;">Selling ${sell.symbol}... (${i + 1}/${plan.sells.length})</span>`);
try {
await new Promise((resolve) => {
$.post(
`https://www.torn.com/page.php?sid=StockMarket&step=sellShares&rfcv=${getRFC()}`,
{ stockId: stockId[sell.symbol], amount: sell.shares }
).done(function(r) {
try {
if (typeof r === "string") r = JSON.parse(r);
if (r.success) {
updateLocalCache(sell.symbol, -sell.shares);
successCount++;
} else {
failCount++;
console.error(`Sell failed for ${sell.symbol}:`, r.text);
}
} catch (e) {
successCount++;
}
resolve();
}).fail(function() {
failCount++;
resolve();
});
});
await new Promise((r) => setTimeout(r, 300));
} catch (e) {
failCount++;
}
}
if (plan.sells.length > 0 && plan.buys.length > 0) {
status.html('<span style="color:#ffd54f;">Processing buys...</span>');
await new Promise((r) => setTimeout(r, 500));
}
for (let i = 0; i < plan.buys.length; i++) {
const buy = plan.buys[i];
status.html(`<span style="color:#ffd54f;">Buying ${buy.symbol}... (${i + 1}/${plan.buys.length})</span>`);
try {
await new Promise((resolve) => {
$.post(
`https://www.torn.com/page.php?sid=StockMarket&step=buyShares&rfcv=${getRFC()}`,
{ stockId: stockId[buy.symbol], amount: buy.shares }
).done(function(r) {
try {
if (typeof r === "string") r = JSON.parse(r);
if (r.success) {
updateLocalCache(buy.symbol, buy.shares);
successCount++;
} else {
failCount++;
console.error(`Buy failed for ${buy.symbol}:`, r.text);
}
} catch (e) {
successCount++;
}
resolve();
}).fail(function() {
failCount++;
resolve();
});
});
await new Promise((r) => setTimeout(r, 300));
} catch (e) {
failCount++;
}
}
updateVaultDisplay();
const totalTrades = plan.sells.length + plan.buys.length;
if (failCount === 0) {
status.html(`<span style="color:#8bc34a;">Rebalance complete! ${successCount}/${totalTrades} trades executed.</span>`);
} else {
status.html(`<span style="color:#ffb74d;">Rebalance done with ${failCount} failed trades. ${successCount}/${totalTrades} succeeded.</span>`);
}
setTimeout(() => status.html(""), 5e3);
}
async function smartWithdraw(targetAmount) {
const status = $("#vault-status");
if (!targetAmount || targetAmount <= 0) {
status.html('<span style="color:#ef5350;">Enter a valid withdrawal amount!</span>');
setTimeout(() => status.html(""), 3e3);
return;
}
if (vaultConfig.stocks.length === 0) {
status.html('<span style="color:#ef5350;">No vault stocks configured! Click Settings to set up your vault.</span>');
setTimeout(() => status.html(""), 3e3);
return;
}
if (vaultConfig.withdrawStrategy === "swing_strategy") {
status.html('<span style="color:#ffd54f;">Analyzing stocks...</span>');
await analyzeAllVaultStocks((msg) => status.html(`<span style="color:#ffd54f;">${msg}</span>`));
}
status.html('<span style="color:#ffd54f;">Calculating withdrawal plan...</span>');
const plan = calculateWithdrawPlan(targetAmount, vaultConfig.withdrawStrategy);
if (plan.length === 0) {
status.html('<span style="color:#ef5350;">Cannot withdraw - no available shares!</span>');
setTimeout(() => status.html(""), 3e3);
return;
}
status.html("");
showWithdrawModal(plan, targetAmount);
}
function calculateWithdrawPlan(targetAmount, strategy) {
let stocks2 = vaultConfig.stocks;
if (stocks2.length === 0) return [];
if (vaultConfig.lockedStocks && vaultConfig.lockedStocks.length > 0) {
stocks2 = stocks2.filter((sym) => !vaultConfig.lockedStocks.includes(sym));
}
if (stocks2.length === 0) {
$("#vault-status").html('<span style="color:#ffb74d;">All stocks are locked!</span>');
return [];
}
const stockInfo = [];
for (const sym of stocks2) {
const owned = getOwnedShares(sym);
const price = getPrice(sym);
if (owned <= 0 || price <= 0) continue;
const stockData = STOCK_DATA[sym];
const tierInfo = getBenefitTier(sym, owned);
const swingProtectedShares = swingTradeData.activePositions.filter((p) => (p.symbol || "").toUpperCase() === (sym || "").toUpperCase()).reduce((sum, p) => sum + (p.shares || 0), 0);
const checkbox = $("#alfa-lock-toggle");
const lockBlocks = checkbox.length > 0 ? checkbox.is(":checked") : localStorage.getItem("alfa_vault_lock") === "true";
let lockedShares = 0;
if (lockBlocks && stockData) {
if (stockData.type === "P") {
lockedShares = owned >= stockData.base ? stockData.base : 0;
} else {
const currentTier = tierInfo.tier;
if (currentTier > 0) {
const minSharesForCurrentTier = stockData.base * (Math.pow(2, currentTier) - 1);
lockedShares = Math.min(minSharesForCurrentTier, owned);
}
}
}
const totalProtected = lockedShares + swingProtectedShares;
const looseShares = Math.max(0, owned - totalProtected);
const value = owned * price;
const looseValue = looseShares * price;
const dailyYield = getDailyYield(sym);
const roi = lockedShares > 0 && price > 0 ? dailyYield * 365 / (lockedShares * price) * 100 : 0;
const analysis = vaultAnalysisCache[sym];
const dipFrom7d = analysis ? analysis.dipFrom7d : 0;
stockInfo.push({
symbol: sym,
owned,
price,
value,
lockedShares: totalProtected,
// Includes benefit tier + swing trade protection
benefitLockedShares: lockedShares,
swingProtectedShares,
looseShares,
looseValue,
tier: tierInfo.tier,
roi,
dipFrom7d,
hasSwingTrade: swingProtectedShares > 0,
analysis
});
}
let plan = [];
switch (strategy) {
case "proportional":
plan = proportionalPlan(targetAmount, stockInfo);
break;
case "swing_strategy":
plan = swingStrategyWithdrawPlan(targetAmount, stockInfo);
break;
case "worst_roi":
plan = worstRoiFirstPlan(targetAmount, stockInfo);
break;
case "best_performers":
plan = bestPerformersPlan(targetAmount, stockInfo);
break;
case "loose_first":
default:
plan = looseSharesFirstPlan(targetAmount, stockInfo);
break;
}
return plan.filter((p) => p.shares > 0);
}
function looseSharesFirstPlan(targetAmount, stockInfo) {
const plan = [];
let remaining = targetAmount;
const lockBlocks = $("#alfa-lock-toggle").is(":checked");
for (const stock of stockInfo) {
if (remaining <= 0) break;
if (stock.looseShares <= 0) continue;
const maxSellValue = Math.min(stock.looseValue, remaining);
const sharesToSell = Math.min(stock.looseShares, Math.ceil(maxSellValue / stock.price));
const sellValue = sharesToSell * stock.price;
if (sharesToSell > 0) {
plan.push({
symbol: stock.symbol,
shares: sharesToSell,
price: stock.price,
value: sellValue,
type: "loose",
swingProtected: stock.hasSwingTrade
});
remaining -= sellValue;
}
}
if (remaining > 0 && !lockBlocks) {
const stocksWithTiers = stockInfo.filter((s) => s.lockedShares > 0).sort((a, b) => a.roi - b.roi);
for (const stock of stocksWithTiers) {
if (remaining <= 0) break;
const alreadyPlanned = plan.find((p) => p.symbol === stock.symbol);
const alreadySelling = alreadyPlanned ? alreadyPlanned.shares : 0;
const availableToSell = stock.owned - alreadySelling;
if (availableToSell <= 0) continue;
const maxSellValue = Math.min(availableToSell * stock.price, remaining);
const sharesToSell = Math.ceil(maxSellValue / stock.price);
const sellValue = sharesToSell * stock.price;
if (sharesToSell > 0) {
if (alreadyPlanned) {
alreadyPlanned.shares += sharesToSell;
alreadyPlanned.value += sellValue;
alreadyPlanned.warning = "Will reduce benefit tier!";
} else {
plan.push({
symbol: stock.symbol,
shares: sharesToSell,
price: stock.price,
value: sellValue,
type: "tier",
warning: "Will reduce benefit tier!",
swingProtected: stock.hasSwingTrade
});
}
remaining -= sellValue;
}
}
}
return plan;
}
function swingStrategyWithdrawPlan(targetAmount, stockInfo) {
const lockBlocks = $("#alfa-lock-toggle").is(":checked");
const maxFractionPerStock = 0.5;
const withScore = stockInfo.filter((s) => s.looseShares > 0).map((s) => {
const opp = s.analysis ? analyzeSwingOpportunity(s.analysis) : null;
const score = opp ? opp.score : 0;
const weight = Math.max(1, 100 - score);
return { ...s, score, weight };
});
const totalWeight = withScore.reduce((sum, s) => sum + s.weight, 0);
const plan = [];
const plannedBySym = {};
let remaining = targetAmount;
if (totalWeight > 0) {
withScore.sort((a, b) => a.score - b.score);
for (const stock of withScore) {
const targetValue = stock.weight / totalWeight * targetAmount;
const capValue = maxFractionPerStock * stock.looseValue;
const maxSellValue = Math.min(stock.looseValue, capValue, targetValue, remaining);
if (maxSellValue <= 0) continue;
const sharesToSell = Math.min(stock.looseShares, Math.floor(maxSellValue / stock.price));
if (sharesToSell <= 0) continue;
const sellValue = sharesToSell * stock.price;
plan.push({
symbol: stock.symbol,
shares: sharesToSell,
price: stock.price,
value: sellValue,
type: "loose",
swingProtected: stock.hasSwingTrade
});
plannedBySym[stock.symbol] = sharesToSell;
remaining -= sellValue;
}
if (remaining > 0) {
const sorted = [...withScore].sort((a, b) => a.score - b.score);
for (const stock of sorted) {
if (remaining <= 0) break;
const alreadySelling = plannedBySym[stock.symbol] || 0;
const looseAfter = stock.looseShares - alreadySelling;
if (looseAfter <= 0) continue;
const alreadyValue = alreadySelling * stock.price;
const capValue = maxFractionPerStock * stock.looseValue;
const roomValue = Math.max(0, capValue - alreadyValue);
const takeValue = Math.min(roomValue, remaining, looseAfter * stock.price);
if (takeValue <= 0) continue;
const addShares = Math.min(looseAfter, Math.floor(takeValue / stock.price));
if (addShares <= 0) continue;
const addValue = addShares * stock.price;
const entry = plan.find((p) => p.symbol === stock.symbol);
if (entry) {
entry.shares += addShares;
entry.value += addValue;
} else {
plan.push({
symbol: stock.symbol,
shares: addShares,
price: stock.price,
value: addValue,
type: "loose",
swingProtected: stock.hasSwingTrade
});
}
plannedBySym[stock.symbol] = (plannedBySym[stock.symbol] || 0) + addShares;
remaining -= addValue;
}
}
}
if (remaining > 0 && !lockBlocks) {
const withTierScore = stockInfo.filter((s) => s.lockedShares > 0).map((s) => {
const opp = s.analysis ? analyzeSwingOpportunity(s.analysis) : null;
const score = opp ? opp.score : 0;
const weight = Math.max(1, 100 - score);
return { ...s, score, weight };
});
const tierTotalWeight = withTierScore.reduce((sum, s) => sum + s.weight, 0);
if (tierTotalWeight > 0) {
withTierScore.sort((a, b) => a.score - b.score);
for (const stock of withTierScore) {
if (remaining <= 0) break;
const alreadyPlanned = plan.find((p) => p.symbol === stock.symbol);
const alreadySelling = alreadyPlanned ? alreadyPlanned.shares : 0;
const availableToSell = stock.owned - alreadySelling;
if (availableToSell <= 0) continue;
const targetValue = stock.weight / tierTotalWeight * remaining;
const tierCapValue = maxFractionPerStock * (stock.owned * stock.price);
const alreadyValue = alreadySelling * stock.price;
const roomValue = Math.max(0, tierCapValue - alreadyValue);
const maxSellValue = Math.min(availableToSell * stock.price, remaining, roomValue, targetValue);
if (maxSellValue <= 0) continue;
const sharesToSell = Math.min(availableToSell, Math.floor(maxSellValue / stock.price));
if (sharesToSell <= 0) continue;
const sellValue = sharesToSell * stock.price;
if (alreadyPlanned) {
alreadyPlanned.shares += sharesToSell;
alreadyPlanned.value += sellValue;
alreadyPlanned.warning = "Will reduce benefit tier!";
} else {
plan.push({
symbol: stock.symbol,
shares: sharesToSell,
price: stock.price,
value: sellValue,
type: "tier",
warning: "Will reduce benefit tier!",
swingProtected: stock.hasSwingTrade
});
}
remaining -= sellValue;
}
if (remaining > 0) {
withTierScore.sort((a, b) => a.score - b.score);
for (const stock of withTierScore) {
if (remaining <= 0) break;
const alreadyPlanned = plan.find((p) => p.symbol === stock.symbol);
const alreadySelling = alreadyPlanned ? alreadyPlanned.shares : 0;
const availableToSell = stock.owned - alreadySelling;
if (availableToSell <= 0) continue;
const tierCapValue = maxFractionPerStock * (stock.owned * stock.price);
const alreadyValue = alreadySelling * stock.price;
const roomValue = Math.max(0, tierCapValue - alreadyValue);
const takeValue = Math.min(roomValue, remaining, availableToSell * stock.price);
if (takeValue <= 0) continue;
const addShares = Math.min(availableToSell, Math.floor(takeValue / stock.price));
if (addShares <= 0) continue;
const addValue = addShares * stock.price;
if (alreadyPlanned) {
alreadyPlanned.shares += addShares;
alreadyPlanned.value += addValue;
alreadyPlanned.warning = "Will reduce benefit tier!";
} else {
plan.push({
symbol: stock.symbol,
shares: addShares,
price: stock.price,
value: addValue,
type: "tier",
warning: "Will reduce benefit tier!",
swingProtected: stock.hasSwingTrade
});
}
remaining -= addValue;
}
}
}
}
return plan;
}
function proportionalPlan(targetAmount, stockInfo) {
const plan = [];
const lockBlocks = $("#alfa-lock-toggle").is(":checked");
let totalSellable = 0;
for (const stock of stockInfo) {
if (lockBlocks) {
totalSellable += stock.looseValue;
} else {
totalSellable += stock.value;
}
}
if (totalSellable <= 0) return plan;
const sellPercentage = Math.min(1, targetAmount / totalSellable);
for (const stock of stockInfo) {
const maxValue = lockBlocks ? stock.looseValue : stock.value;
const maxShares = lockBlocks ? stock.looseShares : stock.owned;
if (maxShares <= 0) continue;
const targetValue = maxValue * sellPercentage;
const sharesToSell = Math.min(maxShares, Math.ceil(targetValue / stock.price));
if (sharesToSell > 0) {
const item = {
symbol: stock.symbol,
shares: sharesToSell,
price: stock.price,
value: sharesToSell * stock.price,
type: sharesToSell > stock.looseShares ? "tier" : "loose",
swingProtected: stock.hasSwingTrade
};
if (!lockBlocks && sharesToSell > stock.looseShares) {
item.warning = "May reduce benefit tier!";
}
plan.push(item);
}
}
return plan;
}
function worstRoiFirstPlan(targetAmount, stockInfo) {
const plan = [];
let remaining = targetAmount;
const lockBlocks = $("#alfa-lock-toggle").is(":checked");
const sortedStocks = [...stockInfo].sort((a, b) => a.roi - b.roi);
for (const stock of sortedStocks) {
if (remaining <= 0) break;
const maxShares = lockBlocks ? stock.looseShares : stock.owned;
if (maxShares <= 0) continue;
const maxValue = maxShares * stock.price;
const targetValue = Math.min(maxValue, remaining);
const sharesToSell = Math.ceil(targetValue / stock.price);
if (sharesToSell > 0) {
const item = {
symbol: stock.symbol,
shares: sharesToSell,
price: stock.price,
value: sharesToSell * stock.price,
type: sharesToSell > stock.looseShares ? "tier" : "loose",
roi: stock.roi,
swingProtected: stock.hasSwingTrade
};
if (!lockBlocks && sharesToSell > stock.looseShares) {
item.warning = "May reduce benefit tier!";
}
plan.push(item);
remaining -= item.value;
}
}
return plan;
}
function bestPerformersPlan(targetAmount, stockInfo) {
const plan = [];
let remaining = targetAmount;
const lockBlocks = $("#alfa-lock-toggle").is(":checked");
const sortedStocks = [...stockInfo].sort((a, b) => b.dipFrom7d - a.dipFrom7d);
for (const stock of sortedStocks) {
if (remaining <= 0) break;
const maxShares = lockBlocks ? stock.looseShares : stock.owned;
if (maxShares <= 0) continue;
const maxValue = maxShares * stock.price;
const targetValue = Math.min(maxValue, remaining);
const sharesToSell = Math.ceil(targetValue / stock.price);
if (sharesToSell > 0) {
const item = {
symbol: stock.symbol,
shares: sharesToSell,
price: stock.price,
value: sharesToSell * stock.price,
type: sharesToSell > stock.looseShares ? "tier" : "loose",
dipFrom7d: stock.dipFrom7d,
swingProtected: stock.hasSwingTrade
};
if (!lockBlocks && sharesToSell > stock.looseShares) {
item.warning = "May reduce benefit tier!";
}
plan.push(item);
remaining -= item.value;
}
}
return plan;
}
function showWithdrawModal(plan, targetAmount) {
const validPlan = plan.filter((p) => p.shares > 0);
const totalAvailable = validPlan.reduce((sum, p) => sum + p.value, 0);
const expectedCash = totalAvailable * 0.999;
const strategyLabel = vaultConfig.withdrawStrategy.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase());
const tradeStates = validPlan.map(() => "pending");
let completedCount = 0;
let cashReceived = 0;
const tradeQueue = validPlan.map((item, idx) => ({
type: "sell",
symbol: item.symbol,
shares: item.shares,
value: item.value,
index: idx
}));
let currentTradeIndex = 0;
function updateProgress() {
completedCount = tradeStates.filter((s) => s === "completed").length;
const progressText = `Progress: ${completedCount}/${validPlan.length} trades completed`;
$("#withdraw-progress").text(progressText);
const netCash = cashReceived * 0.999;
$("#withdraw-cash-received").text(`Cash Received: ~$${formatCompactNumber(netCash)}`);
}
function updateRowState(index, state, value = 0) {
tradeStates[index] = state;
if (state === "completed" && value > 0) {
cashReceived += value;
}
const $row = $(`.rebal-row[data-withdraw-index="${index}"]`);
if (state === "pending") {
$row.removeClass("rebal-row-executing rebal-row-completed rebal-row-failed");
} else if (state === "executing") {
$row.addClass("rebal-row-executing");
} else if (state === "completed") {
$row.removeClass("rebal-row-executing").addClass("rebal-row-completed");
} else if (state === "failed") {
$row.removeClass("rebal-row-executing").addClass("rebal-row-failed");
}
updateProgress();
}
const swingProtectedStocks = swingTradeData.activePositions.map((p) => p.symbol);
const protectedInPlan = swingProtectedStocks.filter(
(sym) => vaultConfig.stocks.includes(sym) && validPlan.some((p) => p.symbol === sym && p.shares > 0)
);
let warningHtml = "";
if (totalAvailable < targetAmount) {
warningHtml = `<div class="rebal-warning" style="background:#ff980022; border-color:#ff980055; color:#ffb74d;">
\u26A0\uFE0F Only $${formatCompactNumber(totalAvailable)} available to withdraw (requested $${formatCompactNumber(targetAmount)})
</div>`;
}
if (protectedInPlan.length > 0) {
warningHtml += `<div class="rebal-warning" style="background:#caa14a22; border-color:#caa14a55; color:#caa14a;">
\u{1F6E1}\uFE0F Swing Trade Protected: ${protectedInPlan.join(", ")}
</div>`;
}
const sellsHtml = `
<div class="rebal-section">
<div class="rebal-section-header" style="color:#ef5350;">\u{1F4C9} Sell Orders (${validPlan.length})</div>
<div class="rebal-table">
${validPlan.map((item, idx) => {
const tipParts = [];
if (item.warning) tipParts.push(item.warning);
if (item.swingProtected) tipParts.push("Swing trade protected");
const tip = tipParts.join(" \xB7 ");
const tipEsc = tip.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
const reasonHtml = tip ? `<span class="rebal-reason rebal-reason-tip" title="${tipEsc}" aria-label="${tipEsc}">\u24D8</span>` : `<span class="rebal-reason"></span>`;
return `<div class="rebal-row" data-withdraw-index="${idx}">
<span class="rebal-sym">${item.symbol}</span>
<span class="rebal-signal" style="color:#ef5350;">SELL</span>
${reasonHtml}
<span class="rebal-shares">-${item.shares.toLocaleString()}</span>
<span class="rebal-value" style="color:#ef5350;">${formatCompactNumber(item.value)}</span>
</div>`;
}).join("")}
</div>
<div class="rebal-subtotal" style="color:#ef5350;">Gross: $${formatCompactNumber(totalAvailable)}</div>
<div class="rebal-subtotal" style="color:#8bc34a; margin-top:4px;">Net (after 0.1% fee): ~$${formatCompactNumber(expectedCash)}</div>
</div>`;
const html = `
<div class="rebal-container">
<div class="rebal-summary">
<div class="rebal-summary-item">
<span class="rebal-summary-label">Strategy</span>
<span class="rebal-summary-value">${strategyLabel}</span>
</div>
<div class="rebal-summary-item">
<span class="rebal-summary-label">Requested</span>
<span class="rebal-summary-value">$${formatCompactNumber(targetAmount)}</span>
</div>
<div class="rebal-summary-item">
<span class="rebal-summary-label">Expected Cash</span>
<span class="rebal-summary-value" style="color:#8bc34a;">~$${formatCompactNumber(expectedCash)}</span>
</div>
</div>
${warningHtml}
${sellsHtml}
<div class="rebal-warning">
\u26A0\uFE0F Click the button to execute trades sequentially. Market prices may vary slightly.
</div>
<div id="withdraw-progress" style="text-align:center; padding:10px; color:#ef5350; font-weight:bold;">Progress: 0/${validPlan.length} trades completed</div>
<div id="withdraw-cash-received" style="text-align:center; padding:5px; color:#8bc34a; font-weight:bold;">Cash Received: ~$0</div>
<div class="rebal-actions">
${tradeQueue.length > 0 ? `<button id="withdraw-cycle-btn" class="alfa-main-btn" style="margin-right:10px;">Selling ${tradeQueue[0].symbol} (1/${tradeQueue.length})</button>` : ""}
<button id="withdraw-cancel" class="alfa-main-btn" style="border-color:#888; color:#888;">Close</button>
</div>
</div>`;
createModal("Smart Withdraw", html);
$("#withdraw-cancel").on("click", function() {
$("#alfa-modal-overlay").remove();
});
if (tradeQueue.length > 0) {
$("#withdraw-cycle-btn").on("click", async function() {
if (currentTradeIndex >= tradeQueue.length) return;
const trade = tradeQueue[currentTradeIndex];
const $btn = $(this);
$btn.prop("disabled", true).text("Executing...");
updateRowState(trade.index, "executing");
try {
await postTradeAsync(trade.symbol, trade.shares, "sellShares");
updateRowState(trade.index, "completed", trade.value);
updateVaultDisplay();
currentTradeIndex++;
if (currentTradeIndex >= tradeQueue.length) {
$btn.prop("disabled", true).text("Completed!").css("background", "#8bc34a").css("color", "#111");
} else {
const nextTrade = tradeQueue[currentTradeIndex];
$btn.prop("disabled", false).text(`Selling ${nextTrade.symbol} (${currentTradeIndex + 1}/${tradeQueue.length})`);
}
} catch (e) {
console.error(`Sell failed for ${trade.symbol}:`, e);
updateRowState(trade.index, "failed");
$btn.prop("disabled", false).text(`Selling ${trade.symbol} (${currentTradeIndex + 1}/${tradeQueue.length}) - Retry`);
}
});
}
}
async function executeWithdrawPlan(plan) {
const status = $("#vault-status");
let successCount = 0;
let failCount = 0;
let totalCash = 0;
for (let i = 0; i < plan.length; i++) {
const item = plan[i];
status.html(`<span style="color:#ffd54f;">Selling ${item.symbol}... (${i + 1}/${plan.length})</span>`);
try {
await new Promise((resolve, reject) => {
$.post(
`https://www.torn.com/page.php?sid=StockMarket&step=sellShares&rfcv=${getRFC()}`,
{ stockId: stockId[item.symbol], amount: item.shares }
).done(function(r) {
try {
if (typeof r === "string") r = JSON.parse(r);
if (r.success) {
updateLocalCache(item.symbol, -item.shares);
successCount++;
totalCash += item.value * 0.999;
resolve();
} else {
failCount++;
console.error(`Sell failed for ${item.symbol}:`, r.text);
resolve();
}
} catch (e) {
successCount++;
totalCash += item.value * 0.999;
resolve();
}
}).fail(function() {
failCount++;
resolve();
});
});
if (i < plan.length - 1) {
await new Promise((r) => setTimeout(r, 300));
}
} catch (e) {
failCount++;
console.error(`Error selling ${item.symbol}:`, e);
}
}
updateVaultDisplay();
if (failCount === 0) {
status.html(`<span style="color:#8bc34a;">Withdrawal complete! ~${formatMoney(totalCash)} received.</span>`);
} else {
status.html(`<span style="color:#ffb74d;">Completed with ${failCount} failed trades. ~${formatMoney(totalCash)} received.</span>`);
}
setTimeout(() => status.html(""), 5e3);
}
function openQuickBuyModal(sym) {
const price = getPriceForTrade(sym);
if (price <= 0) {
const status = $("#vault-status");
status.html('<span style="color:#ef5350;">Price error!</span>');
return;
}
const currentCash = getMoneyFast();
const maxShares = Math.floor(currentCash / price);
const html = `
<div class="quick-trade-container">
<div class="quick-trade-header">
<span class="quick-trade-symbol">${sym}</span>
<span class="quick-trade-price">${formatMoney(price)}</span>
</div>
<div class="quick-trade-info">
<div class="quick-trade-info-row">
<span>Available Cash</span>
<span style="color:#8bc34a;">${formatMoney(currentCash)}</span>
</div>
<div class="quick-trade-info-row">
<span>Max Shares</span>
<span>${maxShares.toLocaleString()}</span>
</div>
</div>
<div class="quick-trade-input-section">
<label class="quick-trade-label">Amount to Buy</label>
<input type="text" id="quick-buy-input" class="alfa-input" style="width:100%;" placeholder="e.g. 100k, 1m, or share count" autofocus>
<div class="quick-trade-hint">Enter dollar amount (100k, 1m) or share count</div>
</div>
<div class="quick-trade-calc" id="quick-buy-calc" style="display:none;">
<div class="quick-trade-calc-row">
<span>Shares</span>
<span id="quick-buy-shares" style="font-weight:bold; color:#fff;">0</span>
</div>
<div class="quick-trade-calc-row highlight">
<span>Total Cost</span>
<span id="quick-buy-cost" style="font-weight:bold; color:#8bc34a;">$0</span>
</div>
<div id="quick-buy-warning" class="quick-trade-warning" style="display:none;"></div>
</div>
<div class="quick-trade-actions">
<button id="quick-buy-execute" class="alfa-main-btn qb-btn-buy" disabled>Buy</button>
<button id="quick-buy-cancel" class="alfa-main-btn qb-btn-close">Cancel</button>
</div>
</div>`;
createModal(`Buy ${sym}`, html);
const $input = $("#quick-buy-input");
const $calc = $("#quick-buy-calc");
const $shares = $("#quick-buy-shares");
const $cost = $("#quick-buy-cost");
const $warning = $("#quick-buy-warning");
const $execute = $("#quick-buy-execute");
$input.on("input", function() {
const input = $(this).val().trim();
if (!input) {
$calc.hide();
$execute.prop("disabled", true);
return;
}
let sharesToBuy = 0;
const parsed = parseTornNumber(input);
if (parsed >= 1e3 && !input.match(/^\d+$/)) {
sharesToBuy = Math.floor(parsed / price);
} else {
sharesToBuy = Math.floor(parsed);
}
if (sharesToBuy <= 0) {
$calc.hide();
$execute.prop("disabled", true);
return;
}
const totalCost = sharesToBuy * price;
$shares.text(sharesToBuy.toLocaleString());
$cost.text(formatMoney(totalCost));
$calc.show();
if (totalCost > currentCash) {
const short = totalCost - currentCash;
$warning.html(`<span style="color:#ef5350;">Insufficient funds! Need ${formatMoney(short)} more.</span>`).show();
$execute.prop("disabled", true);
} else {
$warning.hide();
$execute.prop("disabled", false);
}
});
$execute.on("click", async function() {
const input = $input.val().trim();
if (!input) return;
let sharesToBuy = 0;
const parsed = parseTornNumber(input);
if (parsed >= 1e3 && !input.match(/^\d+$/)) {
sharesToBuy = Math.floor(parsed / price);
} else {
sharesToBuy = Math.floor(parsed);
}
if (sharesToBuy <= 0) return;
const $btn = $(this);
$btn.prop("disabled", true).text("Buying...");
$("#quick-buy-cancel").prop("disabled", true);
try {
await postTradeAsync(sym, sharesToBuy, "buyShares");
$btn.text("Success!").css("background", "#8bc34a").css("color", "#111");
setTimeout(() => {
$("#alfa-modal-overlay").remove();
updateVaultDisplay();
}, 1e3);
} catch (e) {
$btn.text("Error!").css("color", "#ef5350");
console.error("Quick buy error:", e);
setTimeout(() => {
$btn.text("Buy").prop("disabled", false);
$("#quick-buy-cancel").prop("disabled", false);
}, 2e3);
}
});
$("#quick-buy-cancel").on("click", function() {
$("#alfa-modal-overlay").remove();
});
$input.on("keypress", function(e) {
if (e.which === 13 && !$execute.prop("disabled")) {
$execute.click();
}
});
}
function openQuickSellModal(sym) {
const breakdown = calculateVaultBreakdown();
const stock = breakdown.find((s) => s.symbol === sym);
if (!stock || stock.looseShares <= 0) {
const status = $("#vault-status");
if (stock && stock.hasSwingTrade && stock.swingProtectedShares >= stock.owned) {
status.html('<span style="color:#ef5350;">All shares protected by swing trade!</span>');
} else if (stock && stock.benefitLockedShares >= stock.owned) {
status.html('<span style="color:#ef5350;">All shares locked in benefit tier!</span>');
} else {
status.html('<span style="color:#ef5350;">No loose shares to sell!</span>');
}
return;
}
const price = getPriceForTrade(sym);
const looseValue = stock.looseShares * price;
let protectionInfo = "";
if (stock.hasSwingTrade || stock.benefitLockedShares > 0) {
protectionInfo = '<div class="quick-trade-protection">';
if (stock.hasSwingTrade) {
protectionInfo += `<div class="quick-trade-protection-item">\u{1F6E1}\uFE0F Swing trade protected: ${stock.swingProtectedShares.toLocaleString()} shares</div>`;
}
if (stock.benefitLockedShares > 0) {
protectionInfo += `<div class="quick-trade-protection-item">\u{1F512} Benefit tier locked: ${stock.benefitLockedShares.toLocaleString()} shares</div>`;
}
protectionInfo += "</div>";
}
const html = `
<div class="quick-trade-container">
<div class="quick-trade-header">
<span class="quick-trade-symbol">${sym}</span>
<span class="quick-trade-price">${formatMoney(price)}</span>
</div>
<div class="quick-trade-info">
<div class="quick-trade-info-row">
<span>Available Loose Shares</span>
<span style="color:#8bc34a;">${stock.looseShares.toLocaleString()}</span>
</div>
<div class="quick-trade-info-row">
<span>Total Value</span>
<span>${formatMoney(looseValue)}</span>
</div>
</div>
${protectionInfo}
<div class="quick-trade-input-section">
<label class="quick-trade-label">Amount to Sell</label>
<input type="text" id="quick-sell-input" class="alfa-input" style="width:100%;" placeholder='e.g. "all", 50k, 1m, or share count' autofocus>
<div class="quick-trade-hint">Enter "all", dollar amount (50k, 1m), or share count</div>
</div>
<div class="quick-trade-calc" id="quick-sell-calc" style="display:none;">
<div class="quick-trade-calc-row">
<span>Shares</span>
<span id="quick-sell-shares" style="font-weight:bold; color:#fff;">0</span>
</div>
<div class="quick-trade-calc-row">
<span>Gross Value</span>
<span id="quick-sell-gross" style="font-weight:bold; color:#fff;">$0</span>
</div>
<div class="quick-trade-calc-row highlight">
<span>Net Proceeds (after 0.1% fee)</span>
<span id="quick-sell-net" style="font-weight:bold; color:#8bc34a;">$0</span>
</div>
</div>
<div class="quick-trade-actions">
<button id="quick-sell-execute" class="alfa-main-btn qb-btn-sell" disabled>Sell</button>
<button id="quick-sell-cancel" class="alfa-main-btn qb-btn-close">Cancel</button>
</div>
</div>`;
createModal(`Sell ${sym}`, html);
const $input = $("#quick-sell-input");
const $calc = $("#quick-sell-calc");
const $shares = $("#quick-sell-shares");
const $gross = $("#quick-sell-gross");
const $net = $("#quick-sell-net");
const $execute = $("#quick-sell-execute");
$input.on("input", function() {
const input = $(this).val().trim();
if (!input) {
$calc.hide();
$execute.prop("disabled", true);
return;
}
let sharesToSell = 0;
if (input.toLowerCase() === "all") {
sharesToSell = stock.looseShares;
} else {
const parsed = parseTornNumber(input);
if (parsed >= 1e3 && !input.match(/^\d+$/)) {
sharesToSell = Math.ceil(parsed / price);
} else {
sharesToSell = Math.ceil(parsed);
}
}
sharesToSell = Math.min(sharesToSell, stock.looseShares);
if (sharesToSell <= 0) {
$calc.hide();
$execute.prop("disabled", true);
return;
}
const grossValue = sharesToSell * price;
const netValue = grossValue * 0.999;
$shares.text(sharesToSell.toLocaleString());
$gross.text(formatMoney(grossValue));
$net.text(formatMoney(netValue));
$calc.show();
$execute.prop("disabled", false);
});
$execute.on("click", async function() {
const input = $input.val().trim();
if (!input) return;
let sharesToSell = 0;
if (input.toLowerCase() === "all") {
sharesToSell = stock.looseShares;
} else {
const parsed = parseTornNumber(input);
if (parsed >= 1e3 && !input.match(/^\d+$/)) {
sharesToSell = Math.ceil(parsed / price);
} else {
sharesToSell = Math.ceil(parsed);
}
}
sharesToSell = Math.min(sharesToSell, stock.looseShares);
if (sharesToSell <= 0) return;
const $btn = $(this);
$btn.prop("disabled", true).text("Selling...");
$("#quick-sell-cancel").prop("disabled", true);
try {
await postTradeAsync(sym, sharesToSell, "sellShares");
$btn.text("Success!").css("background", "#8bc34a").css("color", "#111");
setTimeout(() => {
$("#alfa-modal-overlay").remove();
updateVaultDisplay();
}, 1e3);
} catch (e) {
$btn.text("Error!").css("color", "#ef5350");
console.error("Quick sell error:", e);
setTimeout(() => {
$btn.text("Sell").prop("disabled", false);
$("#quick-sell-cancel").prop("disabled", false);
}, 2e3);
}
});
$("#quick-sell-cancel").on("click", function() {
$("#alfa-modal-overlay").remove();
});
$input.on("keypress", function(e) {
if (e.which === 13 && !$execute.prop("disabled")) {
$execute.click();
}
});
}
function ensureVaultTradeViewPlacement() {
const $main = $("#vault-main-content");
const $trade = $("#vault-trade-view");
if (!$trade.length || !$main.length) return;
if ($.contains($main[0], $trade[0])) {
$trade.detach().insertAfter($main);
}
}
function closeVaultTradeView() {
$("#vault-trade-view").empty().hide();
$("#vault-main-content").show();
}
function openVaultTradeView(sym, type) {
ensureVaultTradeViewPlacement();
if (type === "buy") {
openVaultBuyView(sym);
} else {
openVaultSellView(sym);
}
}
function openVaultBuyView(sym) {
const price = getPriceForTrade(sym);
if (price <= 0) {
$("#vault-status").html('<span style="color:#ef5350;">Price error!</span>');
return;
}
const currentCash = getMoneyFast();
const maxShares = Math.floor(currentCash / price);
const html = `
<div class="vault-trade-view-inner">
<button id="vault-trade-back" class="alfa-mini-btn" style="border-color:#888; color:#888; margin-bottom:12px;">\u2190 Back to Vault</button>
<div class="vault-trade-header">
<span class="vault-trade-symbol">Buy ${sym}</span>
<span class="vault-trade-price">${formatMoney(price)}</span>
</div>
<div class="quick-trade-container">
<div class="quick-trade-info">
<div class="quick-trade-info-row"><span>Available Cash</span><span style="color:#8bc34a;">${formatMoney(currentCash)}</span></div>
<div class="quick-trade-info-row"><span>Max Shares</span><span>${maxShares.toLocaleString()}</span></div>
</div>
<div class="quick-trade-input-section">
<label class="quick-trade-label">Amount to Buy</label>
<input type="text" id="vault-trade-buy-input" class="alfa-input" style="width:100%;" placeholder="e.g. 100k, 1m, or share count" autofocus>
<div class="quick-trade-hint">Enter dollar amount (100k, 1m) or share count</div>
</div>
<div class="quick-trade-calc" id="vault-trade-buy-calc" style="display:none;">
<div class="quick-trade-calc-row"><span>Shares</span><span id="vault-trade-buy-shares" style="font-weight:bold; color:#fff;">0</span></div>
<div class="quick-trade-calc-row highlight"><span>Total Cost</span><span id="vault-trade-buy-cost" style="font-weight:bold; color:#8bc34a;">$0</span></div>
<div id="vault-trade-buy-warning" class="quick-trade-warning" style="display:none;"></div>
</div>
<div class="quick-trade-actions">
<button id="vault-trade-buy-execute" class="alfa-main-btn qb-btn-buy" disabled>Buy</button>
</div>
</div>
</div>`;
$("#vault-main-content").hide();
$("#vault-trade-view").html(html).show();
$("#vault-trade-back").on("click", closeVaultTradeView);
const $input = $("#vault-trade-buy-input");
const $calc = $("#vault-trade-buy-calc");
const $shares = $("#vault-trade-buy-shares");
const $cost = $("#vault-trade-buy-cost");
const $warning = $("#vault-trade-buy-warning");
const $execute = $("#vault-trade-buy-execute");
$input.on("input", function() {
const input = $(this).val().trim();
if (!input) {
$calc.hide();
$execute.prop("disabled", true);
return;
}
let sharesToBuy = 0;
const parsed = parseTornNumber(input);
if (parsed >= 1e3 && !input.match(/^\d+$/)) sharesToBuy = Math.floor(parsed / price);
else sharesToBuy = Math.floor(parsed);
if (sharesToBuy <= 0) {
$calc.hide();
$execute.prop("disabled", true);
return;
}
const totalCost = sharesToBuy * price;
$shares.text(sharesToBuy.toLocaleString());
$cost.text(formatMoney(totalCost));
$calc.show();
if (totalCost > currentCash) {
$warning.html(`<span style="color:#ef5350;">Insufficient funds! Need ${formatMoney(totalCost - currentCash)} more.</span>`).show();
$execute.prop("disabled", true);
} else {
$warning.hide();
$execute.prop("disabled", false);
}
});
$execute.on("click", async function() {
const input = $input.val().trim();
if (!input) return;
let sharesToBuy = 0;
const parsed = parseTornNumber(input);
if (parsed >= 1e3 && !input.match(/^\d+$/)) sharesToBuy = Math.floor(parsed / price);
else sharesToBuy = Math.floor(parsed);
if (sharesToBuy <= 0) return;
const $btn = $(this);
$btn.prop("disabled", true).text("Buying...");
try {
await postTradeAsync(sym, sharesToBuy, "buyShares");
localStorage.setItem("alfa_stocks_expanded", "true");
$btn.text("Success!").css("background", "#8bc34a").css("color", "#111");
closeVaultTradeView();
updateVaultDisplay();
setTimeout(async () => {
await getLiquidNetworth(false);
updateVaultDisplay();
}, 600);
} catch (e) {
$btn.text("Error!").css("color", "#ef5350");
console.error("Vault buy error:", e);
setTimeout(() => {
$btn.text("Buy").prop("disabled", false);
}, 2e3);
}
});
$input.on("keypress", function(e) {
if (e.which === 13 && !$execute.prop("disabled")) $execute.click();
});
}
function openVaultSellView(sym) {
const breakdown = calculateVaultBreakdown();
const stock = breakdown.find((s) => s.symbol === sym);
if (!stock || stock.looseShares <= 0) {
$("#vault-status").html(stock && stock.hasSwingTrade && stock.swingProtectedShares >= stock.owned ? '<span style="color:#ef5350;">All shares protected by swing trade!</span>' : stock && stock.benefitLockedShares >= stock.owned ? '<span style="color:#ef5350;">All shares locked in benefit tier!</span>' : '<span style="color:#ef5350;">No loose shares to sell!</span>');
return;
}
const price = getPriceForTrade(sym);
const looseValue = stock.looseShares * price;
let protectionInfo = "";
if (stock.hasSwingTrade || stock.benefitLockedShares > 0) {
protectionInfo = '<div class="quick-trade-protection">';
if (stock.hasSwingTrade) protectionInfo += `<div class="quick-trade-protection-item">\u{1F6E1}\uFE0F Swing trade protected: ${stock.swingProtectedShares.toLocaleString()} shares</div>`;
if (stock.benefitLockedShares > 0) protectionInfo += `<div class="quick-trade-protection-item">\u{1F512} Benefit tier locked: ${stock.benefitLockedShares.toLocaleString()} shares</div>`;
protectionInfo += "</div>";
}
const html = `
<div class="vault-trade-view-inner">
<button id="vault-trade-back" class="alfa-mini-btn" style="border-color:#888; color:#888; margin-bottom:12px;">\u2190 Back to Vault</button>
<div class="vault-trade-header">
<span class="vault-trade-symbol">Sell ${sym}</span>
<span class="vault-trade-price">${formatMoney(price)}</span>
</div>
<div class="quick-trade-container">
<div class="quick-trade-info">
<div class="quick-trade-info-row"><span>Available Loose Shares</span><span style="color:#8bc34a;">${stock.looseShares.toLocaleString()}</span></div>
<div class="quick-trade-info-row"><span>Total Value</span><span>${formatMoney(looseValue)}</span></div>
</div>
${protectionInfo}
<div class="quick-trade-input-section">
<label class="quick-trade-label">Amount to Sell</label>
<input type="text" id="vault-trade-sell-input" class="alfa-input" style="width:100%;" placeholder='e.g. "all", 50k, 1m, or share count' autofocus>
<div class="quick-trade-hint">Enter "all", dollar amount (50k, 1m), or share count</div>
</div>
<div class="quick-trade-calc" id="vault-trade-sell-calc" style="display:none;">
<div class="quick-trade-calc-row"><span>Shares</span><span id="vault-trade-sell-shares" style="font-weight:bold; color:#fff;">0</span></div>
<div class="quick-trade-calc-row"><span>Gross Value</span><span id="vault-trade-sell-gross" style="font-weight:bold; color:#fff;">$0</span></div>
<div class="quick-trade-calc-row highlight"><span>Net Proceeds (after 0.1% fee)</span><span id="vault-trade-sell-net" style="font-weight:bold; color:#8bc34a;">$0</span></div>
</div>
<div class="quick-trade-actions">
<button id="vault-trade-sell-execute" class="alfa-main-btn qb-btn-sell" disabled>Sell</button>
</div>
</div>
</div>`;
$("#vault-main-content").hide();
$("#vault-trade-view").html(html).show();
$("#vault-trade-back").on("click", closeVaultTradeView);
const $input = $("#vault-trade-sell-input");
const $calc = $("#vault-trade-sell-calc");
const $shares = $("#vault-trade-sell-shares");
const $gross = $("#vault-trade-sell-gross");
const $net = $("#vault-trade-sell-net");
const $execute = $("#vault-trade-sell-execute");
$input.on("input", function() {
const input = $(this).val().trim();
if (!input) {
$calc.hide();
$execute.prop("disabled", true);
return;
}
let sharesToSell = input.toLowerCase() === "all" ? stock.looseShares : 0;
if (sharesToSell === 0) {
const parsed = parseTornNumber(input);
sharesToSell = parsed >= 1e3 && !input.match(/^\d+$/) ? Math.ceil(parsed / price) : Math.ceil(parsed);
}
sharesToSell = Math.min(sharesToSell, stock.looseShares);
if (sharesToSell <= 0) {
$calc.hide();
$execute.prop("disabled", true);
return;
}
const grossValue = sharesToSell * price;
const netValue = grossValue * 0.999;
$shares.text(sharesToSell.toLocaleString());
$gross.text(formatMoney(grossValue));
$net.text(formatMoney(netValue));
$calc.show();
$execute.prop("disabled", false);
});
$execute.on("click", async function() {
const input = $input.val().trim();
if (!input) return;
let sharesToSell = input.toLowerCase() === "all" ? stock.looseShares : 0;
if (sharesToSell === 0) {
const parsed = parseTornNumber(input);
sharesToSell = parsed >= 1e3 && !input.match(/^\d+$/) ? Math.ceil(parsed / price) : Math.ceil(parsed);
}
sharesToSell = Math.min(sharesToSell, stock.looseShares);
if (sharesToSell <= 0) return;
const $btn = $(this);
$btn.prop("disabled", true).text("Selling...");
try {
await postTradeAsync(sym, sharesToSell, "sellShares");
localStorage.setItem("alfa_stocks_expanded", "true");
$btn.text("Success!").css("background", "#8bc34a").css("color", "#111");
closeVaultTradeView();
updateVaultDisplay();
setTimeout(async () => {
await getLiquidNetworth(false);
updateVaultDisplay();
}, 600);
} catch (e) {
$btn.text("Error!").css("color", "#ef5350");
console.error("Vault sell error:", e);
setTimeout(() => {
$btn.text("Sell").prop("disabled", false);
}, 2e3);
}
});
$input.on("keypress", function(e) {
if (e.which === 13 && !$execute.prop("disabled")) $execute.click();
});
}
function vaultQuickBuy(sym) {
openQuickBuyModal(sym);
}
function vaultQuickSell(sym) {
openQuickSellModal(sym);
}
function toggleStockLock(sym) {
if (!vaultConfig.lockedStocks) {
vaultConfig.lockedStocks = [];
}
const index = vaultConfig.lockedStocks.indexOf(sym);
if (index > -1) {
vaultConfig.lockedStocks.splice(index, 1);
$("#vault-status").html(`<span style="color:#81c784;">${sym} unlocked</span>`);
} else {
vaultConfig.lockedStocks.push(sym);
$("#vault-status").html(`<span style="color:#ffd54f;">${sym} locked - excluded from spread/withdraw</span>`);
}
saveVaultConfig();
setTimeout(() => {
$("#vault-status").html("");
updateVaultDisplay();
}, 1500);
}
function openTransactionHistoryModal(initialPeriod = "all") {
if (portfolioData.transactions.length === 0) {
alert("No transaction history available. Please sync your portfolio first.");
return;
}
let currentHistoryPeriod = localStorage.getItem("alfa_history_period") || initialPeriod;
function buildHistoryContent(period) {
const startTimestamp = getPeriodStartTimestamp(period);
const sortedTx2 = [...portfolioData.transactions].filter((tx) => tx.timestamp >= startTimestamp).sort((a, b) => b.timestamp - a.timestamp);
const plData2 = calculatePortfolioPL();
const periodPL = calculatePeriodPL(period);
const totalBuys2 = sortedTx2.filter((t) => t.type === "buy").length;
const totalSells2 = sortedTx2.filter((t) => t.type === "sell").length;
const totalBuyValue2 = sortedTx2.filter((t) => t.type === "buy").reduce((sum, t) => sum + t.totalValue, 0);
const totalSellValue2 = sortedTx2.filter((t) => t.type === "sell").reduce((sum, t) => sum + t.totalValue, 0);
const periodRealizedPL2 = periodPL.realizedPL;
const periodFees2 = periodPL.fees;
const stockSummary = {};
for (const tx of sortedTx2) {
if (!stockSummary[tx.symbol]) {
stockSummary[tx.symbol] = { buys: 0, sells: 0, buyValue: 0, sellValue: 0, profit: 0 };
}
if (tx.type === "buy") {
stockSummary[tx.symbol].buys += tx.shares;
stockSummary[tx.symbol].buyValue += tx.totalValue;
} else {
stockSummary[tx.symbol].sells += tx.shares;
stockSummary[tx.symbol].sellValue += tx.totalValue;
stockSummary[tx.symbol].profit += tx.profit;
}
}
const stockRows3 = Object.entries(stockSummary).sort((a, b) => b[1].buyValue + b[1].sellValue - (a[1].buyValue + a[1].sellValue)).map(([sym, data]) => {
const stockPL = plData2.perStock[sym];
const unrealizedPL = stockPL ? stockPL.unrealizedPL : 0;
const unrealizedColor = unrealizedPL >= 0 ? "#8bc34a" : "#ef5350";
const realizedColor = data.profit >= 0 ? "#8bc34a" : "#ef5350";
return `<tr>
<td style="font-weight:bold;">${sym}</td>
<td style="text-align:right;">${data.buys.toLocaleString()}</td>
<td style="text-align:right;">${data.sells.toLocaleString()}</td>
<td style="text-align:right;">${formatMoney(data.buyValue)}</td>
<td style="text-align:right;">${formatMoney(data.sellValue)}</td>
<td style="text-align:right; color:${realizedColor};">${data.profit >= 0 ? "+" : ""}${formatMoney(data.profit)}</td>
<td style="text-align:right; color:${unrealizedColor};">${stockPL ? (unrealizedPL >= 0 ? "+" : "") + formatMoney(unrealizedPL) : "-"}</td>
</tr>`;
}).join("");
const txRows2 = sortedTx2.slice(0, 200).map((tx) => {
const date = new Date(tx.timestamp * 1e3);
const dateStr = date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
const typeColor = tx.type === "buy" ? "#66bb6a" : "#ef5350";
const typeIcon = tx.type === "buy" ? "\u25B2" : "\u25BC";
const profitDisplay = tx.type === "sell" ? `<span style="color:${tx.profit >= 0 ? "#8bc34a" : "#ef5350"};">${tx.profit >= 0 ? "+" : ""}${formatMoney(tx.profit)}</span>` : "-";
return `<tr>
<td style="font-size:10px; color:#888;">${dateStr}</td>
<td style="color:${typeColor}; font-weight:bold;">${typeIcon} ${tx.type.toUpperCase()}</td>
<td style="font-weight:bold;">${tx.symbol}</td>
<td style="text-align:right;">${tx.shares.toLocaleString()}</td>
<td style="text-align:right;">${formatMoney(tx.pricePerShare)}</td>
<td style="text-align:right;">${formatMoney(tx.totalValue)}</td>
<td style="text-align:right;">${profitDisplay}</td>
</tr>`;
}).join("");
return { sortedTx: sortedTx2, totalBuys: totalBuys2, totalSells: totalSells2, totalBuyValue: totalBuyValue2, totalSellValue: totalSellValue2, periodRealizedPL: periodRealizedPL2, periodFees: periodFees2, stockRows: stockRows3, txRows: txRows2, plData: plData2 };
}
let contentData = buildHistoryContent(currentHistoryPeriod);
const { sortedTx, totalBuys, totalSells, totalBuyValue, totalSellValue, periodRealizedPL, periodFees, stockRows: stockRows2, txRows, plData } = contentData;
const html = `
<div class="tx-history-container">
<div class="tx-period-filter-row">
<span class="tx-period-label">Period:</span>
<button class="tx-period-btn ${currentHistoryPeriod === "day" ? "active" : ""}" data-period="day">Day</button>
<button class="tx-period-btn ${currentHistoryPeriod === "week" ? "active" : ""}" data-period="week">Week</button>
<button class="tx-period-btn ${currentHistoryPeriod === "month" ? "active" : ""}" data-period="month">Month</button>
<button class="tx-period-btn ${currentHistoryPeriod === "ytd" ? "active" : ""}" data-period="ytd">YTD</button>
<button class="tx-period-btn ${currentHistoryPeriod === "year" ? "active" : ""}" data-period="year">Year</button>
<button class="tx-period-btn ${currentHistoryPeriod === "all" ? "active" : ""}" data-period="all">All</button>
</div>
<div class="tx-tabs">
<button class="tx-tab active" data-tab="summary">Summary</button>
<button class="tx-tab" data-tab="stocks">By Stock</button>
<button class="tx-tab" data-tab="history">Transactions</button>
</div>
<div class="tx-tab-content" id="tab-summary">
<div class="tx-period-badge">${getPeriodLabel(currentHistoryPeriod)}</div>
<div class="tx-summary-grid">
<div class="tx-summary-card">
<span class="tx-summary-label">Buys (${getPeriodLabel(currentHistoryPeriod)})</span>
<span class="tx-summary-value">${totalBuys}</span>
<span class="tx-summary-sub">${formatMoney(totalBuyValue)}</span>
</div>
<div class="tx-summary-card">
<span class="tx-summary-label">Sells (${getPeriodLabel(currentHistoryPeriod)})</span>
<span class="tx-summary-value">${totalSells}</span>
<span class="tx-summary-sub">${formatMoney(totalSellValue)}</span>
</div>
<div class="tx-summary-card ${periodRealizedPL >= 0 ? "pl-positive" : "pl-negative"}">
<span class="tx-summary-label">Realized P&L (${getPeriodLabel(currentHistoryPeriod)})</span>
<span class="tx-summary-value">${periodRealizedPL >= 0 ? "+" : ""}${formatMoney(periodRealizedPL)}</span>
<span class="tx-summary-sub">From ${totalSells} sales</span>
</div>
<div class="tx-summary-card">
<span class="tx-summary-label">Fees (${getPeriodLabel(currentHistoryPeriod)})</span>
<span class="tx-summary-value" style="color:#ff9800;">${formatMoney(periodFees)}</span>
<span class="tx-summary-sub">0.1% sell fee</span>
</div>
<div class="tx-summary-card ${plData.unrealizedPL >= 0 ? "pl-positive" : "pl-negative"}">
<span class="tx-summary-label">Unrealized P&L (Total)</span>
<span class="tx-summary-value">${plData.unrealizedPL >= 0 ? "+" : ""}${formatMoney(plData.unrealizedPL)}</span>
<span class="tx-summary-sub">${plData.unrealizedPLPercent >= 0 ? "+" : ""}${plData.unrealizedPLPercent.toFixed(2)}%</span>
</div>
<div class="tx-summary-card">
<span class="tx-summary-label">Cost Basis (Total)</span>
<span class="tx-summary-value">${formatMoney(plData.totalCostBasis)}</span>
<span class="tx-summary-sub">Current holdings</span>
</div>
</div>
</div>
<div class="tx-tab-content" id="tab-stocks" style="display:none;">
<div style="max-height:400px; overflow-y:auto;">
<table class="tx-table">
<thead>
<tr>
<th>Stock</th>
<th style="text-align:right;">Bought</th>
<th style="text-align:right;">Sold</th>
<th style="text-align:right;">Buy Val</th>
<th style="text-align:right;">Sell Val</th>
<th style="text-align:right;">Realized</th>
<th style="text-align:right;">Unrealized</th>
</tr>
</thead>
<tbody>${stockRows2}</tbody>
</table>
</div>
</div>
<div class="tx-tab-content" id="tab-history" style="display:none;">
<div style="max-height:400px; overflow-y:auto;">
<table class="tx-table">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Stock</th>
<th style="text-align:right;">Shares</th>
<th style="text-align:right;">Price</th>
<th style="text-align:right;">Total</th>
<th style="text-align:right;">P&L</th>
</tr>
</thead>
<tbody>${txRows}</tbody>
</table>
</div>
${sortedTx.length > 200 ? `<div style="text-align:center; padding:10px; color:#888; font-size:11px;">Showing 200 of ${sortedTx.length} transactions</div>` : `<div style="text-align:center; padding:10px; color:#888; font-size:11px;">${sortedTx.length} transactions</div>`}
</div>
<div class="tx-actions">
<button id="tx-export-btn" class="alfa-main-btn" style="border-color:#888; color:#888;">Export CSV</button>
<button id="tx-clear-btn" class="alfa-main-btn" style="border-color:#ef5350; color:#ef5350;">Clear Data</button>
</div>
</div>`;
createModal("Transaction History & P&L", html);
$(".tx-period-btn").on("click", function() {
const newPeriod = $(this).data("period");
localStorage.setItem("alfa_history_period", newPeriod);
closeModal();
openTransactionHistoryModal(newPeriod);
});
$(".tx-tab").on("click", function() {
const tab = $(this).data("tab");
$(".tx-tab").removeClass("active");
$(this).addClass("active");
$(".tx-tab-content").hide();
$(`#tab-${tab}`).show();
});
$("#tx-export-btn").on("click", function() {
exportTransactionCSV();
});
$("#tx-clear-btn").on("click", function() {
if (confirm("This will clear ALL portfolio tracking data. You can re-sync at any time. Continue?")) {
portfolioData = {
transactions: [],
lastSyncTimestamp: 0,
costBasis: {},
realizedPL: 0,
lastFullSync: 0
};
savePortfolioData();
$("#alfa-modal-overlay").remove();
updateVaultDisplay();
}
});
}
function exportTransactionCSV() {
if (portfolioData.transactions.length === 0) {
alert("No transactions to export");
return;
}
const headers = ["Date", "Type", "Symbol", "Shares", "Price Per Share", "Total Value", "Fees", "Profit/Loss"];
const rows = portfolioData.transactions.map((tx) => {
const date = new Date(tx.timestamp * 1e3).toISOString();
return [
date,
tx.type.toUpperCase(),
tx.symbol,
tx.shares,
tx.pricePerShare.toFixed(2),
tx.totalValue,
tx.fees || 0,
tx.profit || 0
].join(",");
});
const csv = [headers.join(","), ...rows].join("\n");
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `torn_stock_transactions_${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.csv`;
a.click();
URL.revokeObjectURL(url);
}
function createModal(title, contentHtml) {
$("#alfa-modal-overlay").remove();
let modal = `<div id="alfa-modal-overlay" class="alfa-modal-overlay hf-torn">
<div class="alfa-modal"><div class="alfa-modal-header"><h3>${title}</h3><span id="alfa-modal-close" class="alfa-modal-close">×</span></div>
<div class="alfa-modal-body">${contentHtml}</div></div></div>`;
$("body").append(modal);
$("#alfa-modal-close").on("click", function() {
$("#alfa-modal-overlay").remove();
});
$("#alfa-modal-overlay").on("click", function(e) {
if (e.target.id === "alfa-modal-overlay") $("#alfa-modal-overlay").remove();
});
}
function closeModal() {
$("#alfa-modal-overlay").remove();
}
function parseTornNumber(val) {
if (typeof val !== "string") return 0;
val = val.trim().toLowerCase();
if (!val) return 0;
if (val.endsWith("k")) return parseFloat(val.replace("k", "")) * 1e3;
if (val.endsWith("m")) return parseFloat(val.replace("m", "")) * 1e6;
if (val.endsWith("b")) return parseFloat(val.replace("b", "")) * 1e9;
return parseFloat(val.replace(/,/g, ""));
}
function formatMoney(amount) {
return "$" + amount.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 2 });
}
function formatMoneyWhole(amount) {
return "$" + formatCompactNumber(amount);
}
function getRFC() {
var c = document.cookie.match(/rfc_v=([^;]+)/);
return c ? c[1] : "";
}
function getBenefitTier(sym, shares) {
const data = STOCK_DATA[sym];
if (!data) return { tier: 0, next: 0, label: "Unknown" };
if (data.type === "P") {
return shares >= data.base ? { tier: 1, next: data.base, label: "Passive" } : { tier: 0, next: data.base, label: "None" };
}
if (shares < data.base) {
return { tier: 0, next: data.base, label: "None" };
}
const tier = Math.floor(Math.log2(shares / data.base + 1));
const next = data.base * (Math.pow(2, tier + 1) - 1);
return { tier, next, label: "Tier " + tier };
}
async function syncWallet(silent = false) {
let key = localStorage.getItem("alfa_vault_apikey");
if (!key) return 0;
if (Date.now() - lastSync < 2e3) return 0;
lastSync = Date.now();
if (!silent) $("#responseStock").html("Syncing...").css("color", "orange");
try {
const response = await fetch(`https://api.torn.com/user/?selections=money&key=${key}&ts=${Date.now()}`);
const data = await response.json();
if (data.money_onhand !== void 0) {
let money = data.money_onhand;
if ($("#user-money").length > 0) $("#user-money").attr("data-money", money).text("$" + money.toLocaleString());
if (!silent) $("#responseStock").html(`Synced: $${money.toLocaleString()}`).css("color", "green");
return money;
}
} catch (e) {
if (!silent) $("#responseStock").html("Sync Failed").css("color", "red");
}
return 0;
}
function getMoneyFast() {
let dataMoney = $("#user-money").attr("data-money");
if (dataMoney) return parseFloat(dataMoney);
let textMoney = $("#user-money").text();
return textMoney ? parseTornNumber(textMoney) : 0;
}
function insert() {
if ($("ul[class^='stock_']").length == 0) {
setTimeout(insert, 500);
return;
}
$("ul[class^='stock_']").each(function() {
let sym = $("img", $(this)).attr("src").split("logos/")[1].split(".svg")[0];
stockId[sym] = $(this).attr("id");
stockRows[sym] = $(this);
stocks[sym] = $(this);
});
buildStockIdMap();
loadVaultConfig();
renderVaultSection();
if (!window.__smartVaultShareCacheHooked) {
let applyExternalStockBuy = function(sym, shares) {
const symbol = String(sym || "").toUpperCase();
const n = Math.floor(Number(shares) || 0);
if (!symbol || n <= 0) return;
updateLocalCache(symbol, n);
updateVaultDisplay(true);
try {
if ($("#gamble-content").is(":visible")) updateGambleCashDisplay();
} catch (e) {
}
};
window.__smartVaultShareCacheHooked = true;
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") invalidateShareCache();
});
window.addEventListener("focus", () => invalidateShareCache());
window.__smartStockVaultApplyBuy = applyExternalStockBuy;
try {
if (typeof unsafeWindow !== "undefined") {
unsafeWindow.__smartStockVaultApplyBuy = applyExternalStockBuy;
}
} catch (e) {
}
document.addEventListener("smart-torn:stock-bought", (ev) => {
const d = ev && ev.detail;
if (!d) return;
applyExternalStockBuy(d.symbol, d.shares);
});
}
}
function getOwnedSharesFromDom(id) {
let row = stockRows[id];
if (!row) return 0;
let mobileEl = row.find("p[class^='count']");
if (mobileEl.length > 0) return parseFloat(mobileEl.text().replace(/,/g, "")) || 0;
let cols = row.children("div");
if (cols.length >= 5) return parseFloat($(cols[4]).text().replace(/,/g, "")) || 0;
return 0;
}
function invalidateShareCache(sym) {
if (sym) delete localShareCache[sym];
else localShareCache = {};
}
function getOwnedShares(id, opts2 = {}) {
if (!opts2.forceDom && localShareCache[id] !== void 0) return localShareCache[id];
const n = getOwnedSharesFromDom(id);
localShareCache[id] = n;
return n;
}
function updateLocalCache(sym, amt) {
const current = localShareCache[sym] !== void 0 ? localShareCache[sym] : getOwnedSharesFromDom(sym);
localShareCache[sym] = Math.max(0, current + amt);
}
function getSharePriceFromRow($row) {
if (!$row || !$row.length) return 0;
const rawId = String($row.attr("id") || "");
const numericId = rawId.replace(/^stock_/i, "");
if (numericId) {
const byId = $row.find(`div.price_${numericId}`);
if (byId.length) {
const price = parseFloat(byId.first().text().replace(/,/g, ""));
if (!Number.isNaN(price) && price > 0) return price;
}
}
let best = 0;
$row.find('div[class*="price_"]').each(function() {
const cls = this.className || "";
if (/price_change|price_diff|price_pct/i.test(cls)) return;
const price = parseFloat($(this).text().replace(/,/g, ""));
if (!Number.isNaN(price) && price > best) best = price;
});
return best;
}
function getPrice(id) {
if (stockRows[id]) return getSharePriceFromRow(stockRows[id]);
return 0;
}
function resolveTradeStockId(symb) {
const raw = stockId[symb];
if (raw) return raw;
const numeric = SYMBOL_TO_ID[symb];
if (numeric) return String(numeric);
return null;
}
function getPriceForTrade(sym) {
const live = getPrice(sym);
if (live > 0) return live;
const analysis = vaultAnalysisCache[sym];
if (analysis && analysis.price > 0) return analysis.price;
return 0;
}
async function postTradeRequest(symb, amt, step) {
const tradeId = resolveTradeStockId(symb);
if (!tradeId) throw new Error("Stock not found on page");
const rfc = getRFC();
const url = `https://www.torn.com/page.php?sid=StockMarket&step=${step}&rfcv=${encodeURIComponent(rfc)}`;
const body = new URLSearchParams();
body.set("stockId", tradeId);
body.set("amount", String(amt));
const res = await fetch(url, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest"
},
body: body.toString()
});
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch (e) {
const snippet = String(text || "").replace(/\s+/g, " ").trim().slice(0, 140);
throw new Error(
"Invalid response from Torn (HTTP " + res.status + (snippet ? ": " + snippet : ", empty body") + ")"
);
}
if (!data.success) {
throw new Error(data.message || data.text || "Trade failed");
}
return data;
}
function getDailyYield(sym) {
let b = ADVISOR_DATA[sym];
if (!b) return 0;
let val = 0;
if (b.type === "cash") val = b.val;
else if (b.type === "item") val = itemPrices[b.id] || 0;
else if (b.type === "manual") val = itemPrices["HRG_AVG"] || 0;
else if (b.type === "average") {
let t = 0, c = 0;
for (let cid of b.ids) {
let p = itemPrices[cid] || 0;
if (p > 0) {
t += p;
c++;
}
}
val = c > 0 ? t / c : 0;
}
return val > 0 && b.freq ? val / b.freq : 0;
}
async function getLiquidNetworth(fastMode = false) {
if (fastMode && lastNwCache) return lastNwCache;
let key = localStorage.getItem("alfa_vault_apikey");
if (!key) return lastNwCache || { liquid: 0, pureCash: 0, wallet: 0, dailyBank: 0, bankActive: false, bankPrincipal: 0 };
try {
const res = await fetch(`https://api.torn.com/v2/user/money?key=${key}`);
const data = await res.json();
if (!data.money) return lastNwCache || { liquid: 0, pureCash: 0, wallet: 0, dailyBank: 0, bankActive: false, bankPrincipal: 0 };
let m = data.money, dailyBank = 0, bankActive = false, bankPrincipal = 0;
if (m.city_bank && m.city_bank.amount > 0) {
bankPrincipal = m.city_bank.amount;
if (m.city_bank.profit > 0 && m.city_bank.duration > 0) dailyBank = m.city_bank.profit / m.city_bank.duration;
bankActive = true;
}
let pureCash = m.wallet || 0;
if (networthSettings.sources.points && m.points > 0) pureCash += m.points * (itemPrices["points"] || 45e3);
const wallet = m.wallet || 0;
lastNwCache = { liquid: 0, pureCash, wallet, dailyBank, bankActive, bankPrincipal, nwFromApi: true };
return lastNwCache;
} catch (e) {
console.error("NW Error", e);
return lastNwCache || { liquid: 0, pureCash: 0, wallet: 0, dailyBank: 0, bankActive: false, bankPrincipal: 0 };
}
}
async function fetchUserPortfolio() {
let key = localStorage.getItem("alfa_vault_apikey");
if (!key) return;
try {
const res = await fetch(`https://api.torn.com/user/?selections=stocks&key=${key}&ts=${Date.now()}`);
const data = await res.json();
if (data.stocks) {
let idToSym = {};
for (let [sym, domId] of Object.entries(stockId)) {
idToSym[domId.replace("stock_", "")] = sym;
}
for (let [sID, sData] of Object.entries(data.stocks)) {
let sym = idToSym[sID];
if (sym) localShareCache[sym] = sData.total_shares || 0;
}
for (let sym of Object.keys(stockRows)) {
localShareCache[sym] = getOwnedSharesFromDom(sym);
}
}
} catch (e) {
console.error("Portfolio Sync Error", e);
}
}
async function sellSmart(sym, shares) {
let price = getPrice(sym);
if (price <= 0) {
alert("Price error.");
return;
}
let confirmMsg = "";
if ($("#alfa-lock-toggle").is(":checked")) {
let owned = getOwnedShares(sym);
let future = getBenefitTier(sym, owned - shares);
let current = getBenefitTier(sym, owned);
if (future.tier < current.tier) {
confirmMsg = `WARNING: Selling this will drop your Block Tier!
`;
}
}
let totalCash = formatMoney(shares * price);
confirmMsg += `Sell ${shares.toLocaleString()} shares of ${sym} for approx ${totalCash}?`;
if (confirm(confirmMsg)) {
await postTrade(sym, shares, "sellShares", `Sold`);
setTimeout(() => updateVaultDisplay(), 1e3);
}
}
async function buySmart(sym, targetShares) {
let money = await syncWallet(true);
let price = getPrice(sym);
if (price <= 0) {
alert("Price error.");
return;
}
let maxAffordable = Math.floor(money / price);
let sharesToBuy = Math.min(maxAffordable, targetShares);
if (sharesToBuy <= 0) {
alert("Not enough cash!");
return;
}
let totalCost = formatMoney(sharesToBuy * price);
if (confirm(`Invest ${totalCost} to buy ${sharesToBuy.toLocaleString()} shares of ${sym}?`)) {
await postTrade(sym, sharesToBuy, "buyShares", `Invested`);
setTimeout(() => updateVaultDisplay(), 1e3);
}
}
function postTrade(symb, amt, step, msg) {
postTradeRequest(symb, amt, step).then(() => {
updateLocalCache(symb, step === "buyShares" ? amt : -amt);
updateVaultDisplay();
setTimeout(async () => {
await getLiquidNetworth(false);
updateVaultDisplay();
}, 600);
}).catch((e) => console.log("Trade error", e));
}
async function postTradeAsync(symb, amt, step) {
await postTradeRequest(symb, amt, step);
updateLocalCache(symb, step === "buyShares" ? amt : -amt);
return { success: true, symbol: symb, amount: amt, step };
}
async function executeGambleDeposit(amount, $btn, statusEl) {
const sym = getGambleTargetStock();
if (!sym || !vaultConfig.stocks.includes(sym)) {
if (statusEl) statusEl.html('<span style="color:#ef5350;">No target stock configured. Add vault stocks in Settings.</span>');
return;
}
if (vaultConfig.lockedStocks && vaultConfig.lockedStocks.includes(sym)) {
if (statusEl) statusEl.html('<span style="color:#ef5350;">Target stock is locked.</span>');
return;
}
if (!vaultAnalysisCache[sym]) await analyzeStockForVault(sym);
const price = getPriceForTrade(sym);
if (price <= 0) {
if (statusEl) statusEl.html('<span style="color:#ef5350;">Price unavailable.</span>');
return;
}
let cash = 0;
const domCash = getMoneyFast();
if (domCash !== void 0 && domCash !== null && !isNaN(domCash) && $("#user-money").length > 0) {
cash = domCash;
} else {
const nw = await getLiquidNetworth(true) || await getLiquidNetworth(false);
cash = (nw.wallet !== void 0 ? nw.wallet : nw.pureCash) || 0;
}
let sharesToBuy = 0;
if (amount < 0) {
sharesToBuy = Math.floor(cash / price);
} else {
const cost = amount;
sharesToBuy = Math.floor(cost / price);
if (sharesToBuy * price > cash) {
if (statusEl) statusEl.html(`<span style="color:#ef5350;">Cannot deposit ${formatMoneyWhole(amount)}: only ${formatMoneyWhole(cash)} available. All or nothing.</span>`);
setTimeout(() => {
if (statusEl) statusEl.html("");
}, 5e3);
return;
}
}
if (sharesToBuy <= 0) {
if (statusEl) statusEl.html('<span style="color:#ef5350;">No cash to deposit.</span>');
return;
}
if ($btn) $btn.prop("disabled", true).data("orig-text", $btn.text()).text("...");
try {
await postTradeAsync(sym, sharesToBuy, "buyShares");
if (statusEl) statusEl.html(`<span style="color:#8bc34a;">Deposited ${sharesToBuy.toLocaleString()} shares (${formatMoneyWhole(sharesToBuy * price)})</span>`);
updateVaultDisplay();
} catch (e) {
if (statusEl) statusEl.html(`<span style="color:#ef5350;">${e.message || "Failed"}</span>`);
console.error("Gamble deposit error:", e);
}
if ($btn) {
$btn.prop("disabled", false).text($btn.data("orig-text") || "Deposit");
setTimeout(() => {
if (statusEl) statusEl.html("");
}, 3e3);
}
}
async function executeGambleWithdraw(amount, $btn, statusEl) {
const sym = getGambleTargetStock();
if (!sym || !vaultConfig.stocks.includes(sym)) {
if (statusEl) statusEl.html('<span style="color:#ef5350;">No target stock configured.</span>');
return;
}
if (vaultConfig.lockedStocks && vaultConfig.lockedStocks.includes(sym)) {
if (statusEl) statusEl.html('<span style="color:#ef5350;">Target stock is locked. Unlock in vault to withdraw.</span>');
return;
}
const breakdown = calculateVaultBreakdown();
const stock = breakdown.find((s) => s.symbol === sym);
if (!stock || stock.looseShares <= 0) {
if (statusEl) {
if (stock && stock.hasSwingTrade && stock.swingProtectedShares >= stock.owned) {
statusEl.html('<span style="color:#ef5350;">All shares protected by swing trade!</span>');
} else if (stock && stock.benefitLockedShares >= stock.owned) {
statusEl.html('<span style="color:#ef5350;">All shares locked in benefit tier!</span>');
} else {
statusEl.html('<span style="color:#ef5350;">No loose shares to withdraw.</span>');
}
}
return;
}
if (!vaultAnalysisCache[sym]) await analyzeStockForVault(sym);
const price = getPriceForTrade(sym);
if (price <= 0) {
if (statusEl) statusEl.html('<span style="color:#ef5350;">Price unavailable.</span>');
return;
}
const maxSellable = Math.max(0, stock.owned - stock.swingProtectedShares - stock.benefitLockedShares);
let maxByValue = maxSellable;
if (stock.looseValue > 0 && price > 0) {
maxByValue = Math.min(maxSellable, Math.floor(stock.looseValue / price));
}
const maxSellableShares = Math.min(stock.looseShares, maxByValue);
let sharesToSell = 0;
if (amount < 0) {
sharesToSell = maxSellableShares;
} else {
const sharesRequested = Math.ceil(amount / price);
if (sharesRequested > maxSellableShares) {
if (statusEl) statusEl.html(`<span style="color:#ef5350;">Cannot withdraw ${formatMoneyWhole(amount)}: only ${formatMoneyWhole(stock.looseValue)} loose available (${maxSellableShares.toLocaleString()} shares). All or nothing.</span>`);
setTimeout(() => {
if (statusEl) statusEl.html("");
}, 5e3);
return;
}
sharesToSell = sharesRequested;
}
if (sharesToSell <= 0) {
if (statusEl) statusEl.html('<span style="color:#ef5350;">No shares to sell.</span>');
return;
}
if ($btn) $btn.prop("disabled", true).data("orig-text", $btn.text()).text("...");
try {
await postTradeAsync(sym, sharesToSell, "sellShares");
if (statusEl) statusEl.html(`<span style="color:#8bc34a;">Withdrew ${sharesToSell.toLocaleString()} shares (${formatMoneyWhole(sharesToSell * price * 0.999)})</span>`);
updateVaultDisplay();
} catch (e) {
if (statusEl) statusEl.html(`<span style="color:#ef5350;">${e.message || "Failed"}</span>`);
console.error("Gamble withdraw error:", e);
}
if ($btn) {
$btn.prop("disabled", false).text($btn.data("orig-text") || "Withdraw");
setTimeout(() => {
if (statusEl) statusEl.html("");
}, 3e3);
}
}
async function executeGamblePanic($btn, statusEl) {
await executeGambleDeposit(-1, $btn, statusEl);
}
async function updateGambleCashDisplay() {
const el = $("#gamble-cash-value");
const availEl = $("#gamble-available-value");
if (!el.length) return;
try {
const domCash = getMoneyFast();
if (domCash !== void 0 && domCash !== null && !isNaN(domCash) && $("#user-money").length > 0) {
if (lastNwCache) {
lastNwCache.wallet = domCash;
lastNwCache.pureCash = domCash;
}
el.text(formatMoneyWhole(domCash));
} else {
const nw = await getLiquidNetworth(true) || await getLiquidNetworth(false);
el.text(formatMoneyWhole((nw.wallet !== void 0 ? nw.wallet : nw.pureCash) || 0));
}
} catch (e) {
el.text("--");
}
try {
if (!availEl.length) return;
const sym = getGambleTargetStock();
if (!sym || !vaultConfig.stocks.includes(sym)) {
availEl.text("--");
return;
}
if (vaultConfig.lockedStocks && vaultConfig.lockedStocks.includes(sym)) {
availEl.text("$0");
return;
}
const breakdown = calculateVaultBreakdown();
const stock = breakdown.find((s) => s.symbol === sym);
if (!stock || stock.looseShares <= 0) {
availEl.text("$0");
return;
}
availEl.text(formatMoneyWhole(stock.looseValue));
} catch (e) {
if (availEl.length) availEl.text("--");
}
}
function openGambleConfigModal() {
const presets = gambleConfig.depositPresets || DEFAULT_GAMBLE_CONFIG.depositPresets;
const wPresets = gambleConfig.withdrawPresets || DEFAULT_GAMBLE_CONFIG.withdrawPresets;
const depositRows = presets.map((p, i) => `
<div class="gamble-config-row">
<input type="text" class="gamble-config-label alfa-input" data-type="deposit" data-i="${i}" value="${p.label}" placeholder="Label" style="width:60px;">
<input type="text" class="gamble-config-amount alfa-input" data-type="deposit" data-i="${i}" value="${p.amount < 0 ? "all" : p.amount.toLocaleString()}" placeholder="Amount or 'all'" style="width:100px;">
</div>
`).join("");
const withdrawRows = wPresets.map((p, i) => `
<div class="gamble-config-row">
<input type="text" class="gamble-config-label alfa-input" data-type="withdraw" data-i="${i}" value="${p.label}" placeholder="Label" style="width:60px;">
<input type="text" class="gamble-config-amount alfa-input" data-type="withdraw" data-i="${i}" value="${p.amount < 0 ? "all" : p.amount.toLocaleString()}" placeholder="Amount or 'all'" style="width:100px;">
</div>
`).join("");
const html = `
<div class="gamble-config-modal">
<div class="gamble-config-section">
<div class="gamble-config-title">Deposit Presets</div>
${depositRows}
</div>
<div class="gamble-config-section">
<div class="gamble-config-title">Withdraw Presets</div>
${withdrawRows}
</div>
<div class="gamble-config-hint">Use "all" for full amount. Amounts support k, m (e.g. 50k, 1m).</div>
<div class="gamble-config-actions">
<button id="gamble-config-save" class="alfa-main-btn" style="border-color:#66bb6a; color:#66bb6a;">Save</button>
<button id="gamble-config-cancel" class="alfa-main-btn" style="border-color:#888; color:#888;">Cancel</button>
</div>
</div>`;
createModal("Gamble Presets", html);
$("#gamble-config-save").on("click", function() {
const dep = gambleConfig.depositPresets || [...DEFAULT_GAMBLE_CONFIG.depositPresets];
const wit = gambleConfig.withdrawPresets || [...DEFAULT_GAMBLE_CONFIG.withdrawPresets];
$(".gamble-config-label[data-type=deposit]").each(function() {
const i = parseInt($(this).data("i"), 10);
dep[i] = dep[i] || {};
dep[i].label = $(this).val().trim() || "Preset";
});
$(".gamble-config-amount[data-type=deposit]").each(function() {
const i = parseInt($(this).data("i"), 10);
const val = $(this).val().trim().toLowerCase();
dep[i] = dep[i] || {};
dep[i].amount = val === "all" ? -1 : parseTornNumber($(this).val()) || 0;
});
$(".gamble-config-label[data-type=withdraw]").each(function() {
const i = parseInt($(this).data("i"), 10);
wit[i] = wit[i] || {};
wit[i].label = $(this).val().trim() || "Preset";
});
$(".gamble-config-amount[data-type=withdraw]").each(function() {
const i = parseInt($(this).data("i"), 10);
const val = $(this).val().trim().toLowerCase();
wit[i] = wit[i] || {};
wit[i].amount = val === "all" ? -1 : parseTornNumber($(this).val()) || 0;
});
gambleConfig.depositPresets = dep;
gambleConfig.withdrawPresets = wit;
saveGambleConfig();
$("#alfa-modal-overlay").remove();
updateVaultDisplay();
});
$("#gamble-config-cancel").on("click", function() {
$("#alfa-modal-overlay").remove();
});
}
async function fetchBankRates() {
let key = localStorage.getItem("alfa_vault_apikey");
if (!key) {
alert("API Key missing");
return;
}
const fetchBtn = $("#settings-fetch-bank, #adv-fetch-bank");
fetchBtn.text("Fetching...");
try {
const resRates = await fetch(`https://api.torn.com/v2/torn?selections=bank&key=${key}`);
const dataRates = await resRates.json();
const resPerks = await fetch(`https://api.torn.com/user/?selections=perks&key=${key}`);
const dataPerks = await resPerks.json();
if (dataRates.error) throw new Error(dataRates.error.error);
const parseInterest = (list) => {
if (!list || !Array.isArray(list)) return 0;
let bonus = 0;
list.forEach((str) => {
if (str.toLowerCase().includes("bank interest")) {
let match = str.match(/(\d+(?:\.\d+)?)%/);
if (match) bonus += parseFloat(match[1]);
}
});
return bonus;
};
let totalBonus = 0;
totalBonus += parseInterest(dataPerks.merit_perks);
totalBonus += parseInterest(dataPerks.faction_perks);
totalBonus += parseInterest(dataPerks.job_perks);
totalBonus += parseInterest(dataPerks.property_perks);
totalBonus += parseInterest(dataPerks.stock_perks);
totalBonus += parseInterest(dataPerks.education_perks);
totalBonus += parseInterest(dataPerks.book_perks);
let multi = 1 + totalBonus / 100;
let bankData = dataRates.bank;
if (bankData) {
["1w", "2w", "1m", "2m", "3m"].forEach((term) => {
if (bankData[term]) {
let baseApr = parseFloat(bankData[term]);
let finalApr = baseApr * multi;
$(`#bank-${term}`).val(finalApr.toFixed(2)).trigger("change");
bankSettings["roi_" + term] = finalApr;
}
});
localStorage.setItem("alfa_advisor_bank", JSON.stringify(bankSettings));
fetchBtn.text("Updated!");
} else {
fetchBtn.text("No Data");
}
} catch (e) {
console.error("Bank Fetch Error:", e);
fetchBtn.text("Error");
}
setTimeout(() => fetchBtn.text("Fetch Rates (API)"), 2e3);
}
function startVaultApp() {
opts.registerSettings?.(openSettingsModal);
insert();
}
function whenReady() {
const jq = resolveJQuery();
if (!jq) {
return false;
}
$ = jq;
return jq(MOUNT_SELECTOR).length > 0;
}
const deadline = Date.now() + 3e4;
const tick = () => {
if (whenReady()) {
startVaultApp();
return;
}
if (Date.now() > deadline) {
console.error("[Smart Stock Vault] jQuery or panel mount not ready after 30s");
return;
}
window.setTimeout(tick, 50);
};
tick();
}
// src/stocks/legacy-styles.js
var LEGACY_STYLES = `
.alfa-card-head { cursor: pointer; user-select: none; } .alfa-card-head:hover .alfa-card-title { color: #fff; }
.alfa-card-details { display: none; margin-top: 10px; padding-top: 8px; border-top: 1px dashed #444; font-size: 11px; color: #ccc; }
.alfa-detail-row { display: flex; justify-content: space-between; padding: 2px 0; }
.alfa-detail-sub { color: #888; padding-left: 8px; }
.alfa-detail-total { border-top: 1px solid #333; margin-top: 4px; padding-top: 4px; font-weight: bold; color: #8bc34a; }
.alfa-detail-miss { border-top: 1px solid #333; margin-top: 4px; padding-top: 4px; font-weight: bold; color: #ef5350; }
.alfa-hero { cursor: pointer; transition: background 0.2s; background: linear-gradient(135deg, #2a2a2a 0%, #222 100%); border: 1px solid #333; border-radius: 8px; padding: 20px; text-align: center; position: relative; }
.alfa-hero:hover { border-color: var(--hf-accent, #caa14a); }
.alfa-breakdown { display: none; margin-top: 15px; border-top: 1px solid #444; padding-top: 10px; text-align: left; }
.alfa-break-row { display: flex; flex-direction: column; font-size: 11px; padding: 6px 0; border-bottom: 1px solid #222; color: #ccc; }
.alfa-break-row:last-child { border-bottom: none; }
.alfa-break-val { color: #8bc34a; font-weight: bold; }
.alfa-caret { float: right; transition: transform 0.3s; font-size: 10px; color: #666; }
.alfa-expanded .alfa-caret { transform: rotate(180deg); }
.alfa-header { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 10px; margin-bottom: 10px; border-bottom: 1px solid #333; padding-bottom: 10px; }
.alfa-toolbar { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-top: 5px; font-size: 11px; color: #888; }
.alfa-small-label { display: flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; } .alfa-small-label:hover { color: #fff; }
.alfa-advisor-btn { background: #2a4040; border: 1px solid var(--hf-accent, #caa14a); color: #fff; padding: 4px 10px; border-radius: 4px; font-size: 11px; font-weight: bold; cursor: pointer; text-decoration: none; }
.alfa-advisor-btn:hover { background: var(--hf-accent, #caa14a); }
.alfa-input, .alfa-select { background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; padding: 4px 8px; height: 28px; box-sizing: border-box; }
.alfa-input { width: 130px; } .alfa-select { width: 150px; line-height: 20px; } .alfa-input:focus, .alfa-select:focus { border-color: var(--hf-accent, #caa14a); outline: none; }
textarea.alfa-input { height: auto; min-height: 60px; }
.journal-notes-textarea { width: 100% !important; min-height: 80px; box-sizing: border-box; }
.alfa-link { cursor: pointer; color: var(--hf-accent, #caa14a); text-decoration: underline; margin-left: auto; } .alfa-link:hover { color: #fff; }
.alfa-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 99999; display: flex; justify-content: center; align-items: center; backdrop-filter: blur(4px); }
.alfa-modal { background: #1e1e1e; width: 600px; max-width: 95%; border: 1px solid #333; border-radius: 12px; box-shadow: 0 20px 50px rgba(0,0,0,0.9); display: flex; flex-direction: column; overflow: hidden; animation: fadeIn 0.2s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.alfa-modal-header { background: #252525; padding: 15px 20px; border-bottom: 1px solid #333; display: flex; justify-content: space-between; align-items: center; }
.alfa-modal-header h3 { margin: 0; font-size: 16px; font-weight: 600; color: #fff; letter-spacing: 0.5px; }
.alfa-modal-close { cursor: pointer; font-size: 24px; color: #666; transition: color 0.2s; } .alfa-modal-close:hover { color: #fff; }
.alfa-modal-body { padding: 20px; color: #ccc; overflow-y: auto; max-height: 80vh; }
.alfa-dashboard { display: flex; flex-direction: column; gap: 20px; }
.alfa-hero-label { font-size: 11px; text-transform: uppercase; color: #888; letter-spacing: 1px; margin-bottom: 5px; }
.alfa-hero-val { font-size: 28px; font-weight: 700; color: #8bc34a; text-shadow: 0 2px 4px rgba(0,0,0,0.3); }
.alfa-hero-sub { font-size: 12px; color: #666; margin-top: 5px; }
.alfa-grid-section { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.alfa-card { background: #252525; border: 1px solid #333; border-radius: 8px; padding: 15px; display: flex; flex-direction: column; transition: transform 0.2s; } .alfa-card:hover { border-color: #444; transform: translateY(-2px); }
.alfa-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #333; padding-bottom: 8px; }
.alfa-card-title { font-size: 11px; font-weight: bold; color: #aaa; text-transform: uppercase; }
.alfa-card-roi { font-size: 18px; font-weight: bold; color: var(--hf-accent, #caa14a); }
.alfa-card-body { flex-grow: 1; display: flex; flex-direction: column; gap: 4px; }
.alfa-stock-name { font-size: 14px; font-weight: bold; color: #fff; }
.alfa-stock-cost { font-size: 12px; color: #888; }
.alfa-stock-gain { font-size: 12px; color: #8bc34a; margin-top: auto; padding-top: 10px; display: flex; align-items: center; gap: 5px; } .alfa-stock-gain::before { content: "\u25B2"; font-size: 8px; margin-right: 4px; }
.alfa-actions { display: flex; gap: 10px; border-top: 1px solid #333; padding-top: 20px; margin-top: 10px; }
.alfa-btn-main { flex: 1; background: #333; color: #fff; border: 1px solid #444; padding: 10px; border-radius: 6px; cursor: pointer; font-weight: bold; transition: all 0.2s; text-align: center; } .alfa-btn-main:hover { background: #444; border-color: var(--hf-accent, #caa14a); }
.alfa-settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.alfa-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 12px; }
.alfa-table th { text-align: left; padding: 8px; color: #888; border-bottom: 1px solid #444; }
.alfa-table td { padding: 8px; border-bottom: 1px solid #333; }
.alfa-check-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; max-height: 200px; overflow-y: auto; background: #151515; padding: 10px; border-radius: 6px; border: 1px solid #333; }
.alfa-check-label { display: flex; align-items: center; gap: 8px; color: #ccc; cursor: pointer; font-size: 11px; } .alfa-check-label:hover { color: #fff; }
.alfa-tbl-input { width: 100%; background: #222; border: 1px solid #444; color: #fff; padding: 4px; text-align: right; border-radius: 4px; box-sizing: border-box; }
.alfa-shortage { color: #ef5350; font-weight: bold; }
.alfa-main-btn { background: #333; color: #ddd; border: 1px solid #555; border-radius: 4px; padding: 0 15px; height: 28px; font-size: 12px; font-weight: bold; cursor: pointer; transition: all 0.2s; } .alfa-main-btn:hover { background: #444; border-color: var(--hf-accent, #caa14a); color: #fff; }
.alfa-mini-btn { background: transparent; border: 1px solid #ef5350; color: #ef5350; font-size: 9px; padding: 1px 6px; border-radius: 3px; cursor: pointer; margin-left: 8px; text-transform: uppercase; }
.alfa-mini-btn:hover { background: #ef5350; color: #fff; }
#vault-trade-back.alfa-mini-btn:hover { background: var(--hf-accent, #caa14a); border-color: var(--hf-accent, #caa14a); color: #fff; }
.alfa-invest-btn { width: 100%; background: #2a4040; border: 1px solid var(--hf-accent, #caa14a); color: #fff; padding: 6px; margin-top: 8px; font-size: 11px; font-weight: bold; cursor: pointer; border-radius: 4px; }
.alfa-invest-btn:hover { background: var(--hf-accent, #caa14a); }
/* --- DIAGNOSTIC TOOL CSS --- */
.sim-row { transition: background 0.2s; }
.sim-row:hover { background: rgba(255,255,255,0.05) !important; }
/* Ensure inputs in the modal look right */
.alfa-tbl-input { background: #111; border: 1px solid #444; color: #fff; padding: 4px; border-radius: 4px; }
/* --- MULTI-STOCK VAULT CSS --- */
.alfa-vault-section { background: #111; padding: 12px; border: 1px solid #333; border-radius: 8px; margin-bottom: 15px; color: #ccc; font-family: Arial, sans-serif; font-size: 12px; }
.alfa-vault-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.alfa-vault-api-row { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid #333; }
.alfa-vault-title { font-size: 14px; font-weight: bold; color: var(--hf-accent, #caa14a); }
.alfa-vault-actions { display: flex; gap: 5px; }
.alfa-vault-empty { text-align: center; padding: 20px; color: #888; }
.alfa-vault-summary { display: flex; justify-content: space-between; align-items: center; background: linear-gradient(135deg, #1a2a2a 0%, #151515 100%); padding: 12px; border-radius: 6px; margin-bottom: 12px; }
.alfa-vault-total-label { font-size: 10px; text-transform: uppercase; color: #888; display: block; margin-bottom: 2px; }
.alfa-vault-total-value { font-size: 20px; font-weight: bold; color: #8bc34a; }
.alfa-vault-meta { font-size: 11px; color: #666; }
.alfa-vault-breakdown { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; max-height: 200px; overflow-y: auto; }
.vault-stock-row { background: #1a1a1a; padding: 8px 10px; border-radius: 4px; border: 1px solid #252525; transition: border-color 0.2s; }
.vault-stock-row:hover { border-color: #444; }
.vault-stock-info { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.vault-stock-name { font-weight: bold; color: #fff; min-width: 35px; }
.vault-swing-badge { font-size: 10px; cursor: help; }
.vault-stock-value { color: #8bc34a; font-weight: bold; }
.vault-stock-pct { color: #666; font-size: 10px; }
.vault-signal-badge { font-size: 9px; padding: 1px 5px; border-radius: 3px; border: 1px solid; font-weight: bold; }
.vault-stock-bar-container { height: 6px; background: #252525; border-radius: 3px; overflow: hidden; margin-bottom: 4px; }
.vault-stock-bar { height: 100%; background: linear-gradient(90deg, var(--hf-accent, #caa14a), #4a7a7a); border-radius: 3px; position: relative; min-width: 2px; transition: width 0.3s; }
.vault-stock-bar-loose { position: absolute; right: 0; top: 0; height: 100%; background: rgba(139, 195, 74, 0.5); }
.vault-stock-details { display: flex; gap: 10px; font-size: 10px; }
.vault-dip-neg { color: #ef5350; }
.vault-dip-pos { color: #8bc34a; }
.vault-rsi { color: #888; }
.vault-rsi.oversold { color: #66bb6a; font-weight: bold; }
.vault-rsi.overbought { color: #ef5350; font-weight: bold; }
.vault-loose { color: #81c784; }
.alfa-vault-controls { display: flex; flex-wrap: wrap; gap: 15px; justify-content: space-between; padding: 10px 0; border-top: 1px solid #333; margin-bottom: 8px; }
.alfa-vault-control-group { display: flex; gap: 5px; align-items: center; }
@media (max-width: 600px) {
.alfa-vault-controls { flex-direction: column; align-items: stretch; gap: 10px; }
.alfa-vault-control-group { justify-content: flex-start; flex-wrap: wrap; }
.alfa-vault-control-group:last-child { flex-wrap: wrap; }
#vault-withdraw-amt { flex: 1; min-width: 60px; max-width: 120px; }
}
.alfa-vault-options { display: flex; gap: 20px; padding: 8px 0; border-bottom: 1px solid #333; margin-bottom: 10px; }
.vault-option-label { display: flex; align-items: center; gap: 6px; font-size: 11px; color: #888; cursor: pointer; }
.vault-option-label:hover { color: #fff; }
.vault-option-label input:checked + span { color: #8bc34a; }
.vault-spread-btn { border-color: #66bb6a !important; color: #66bb6a !important; }
.vault-spread-btn:hover { background: #66bb6a !important; color: #111 !important; }
.vault-withdraw-btn { border-color: #ef5350 !important; color: #ef5350 !important; }
.vault-withdraw-btn:hover { background: #ef5350 !important; color: #fff !important; }
/* --- SETTINGS MODAL CSS --- */
.settings-tabs { display: flex; gap: 5px; margin-bottom: 15px; border-bottom: 1px solid #333; padding-bottom: 10px; }
.settings-tab { background: transparent; border: 1px solid #444; color: #888; padding: 8px 16px; border-radius: 4px 4px 0 0; cursor: pointer; font-size: 11px; text-transform: uppercase; transition: all 0.2s; }
.settings-tab:hover { color: #fff; border-color: var(--hf-accent, #caa14a); }
.settings-tab.active { background: var(--hf-accent, #caa14a); color: #fff; border-color: var(--hf-accent, #caa14a); }
.settings-content { min-height: 300px; }
.settings-tab-content { animation: fadeIn 0.2s ease-out; }
.settings-section { background: #1a1a1a; border: 1px solid #333; border-radius: 6px; padding: 12px; margin-bottom: 12px; }
.settings-section-title { font-size: 11px; font-weight: bold; color: var(--hf-accent, #caa14a); text-transform: uppercase; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }
.settings-stock-count { font-size: 10px; color: #888; font-weight: normal; }
.settings-row { display: flex; gap: 10px; }
.settings-warning { background: #2a2a20; border: 1px solid #666633; border-radius: 4px; padding: 10px; font-size: 11px; color: #ffd54f; margin-top: 10px; }
.settings-api-status { text-align: center; font-size: 11px; padding: 8px; background: #151515; border-radius: 4px; }
.settings-checkbox-label { display: flex; align-items: flex-start; gap: 10px; padding: 8px; background: #151515; border-radius: 4px; margin-bottom: 8px; cursor: pointer; font-size: 11px; color: #ccc; }
.settings-checkbox-label:hover { background: #1a1a1a; }
.settings-checkbox-label input[type="checkbox"] { margin-top: 2px; }
.settings-checkbox-label strong { color: #fff; }
.settings-note { font-size: 10px; color: #666; margin-bottom: 8px; }
.settings-exclude-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; max-height: 150px; overflow-y: auto; background: #151515; padding: 10px; border-radius: 4px; }
.settings-exclude-item { display: flex; align-items: center; gap: 5px; font-size: 10px; color: #888; cursor: pointer; }
.settings-exclude-item:hover { color: #fff; }
.settings-bank-inputs { display: flex; flex-direction: column; gap: 6px; }
.settings-actions { display: flex; gap: 10px; margin-top: 15px; padding-top: 15px; border-top: 1px solid #333; }
/* --- DAILY INCOME ROW CSS --- */
.alfa-vault-income-row { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: linear-gradient(135deg, #1a2520 0%, #151515 100%); border-radius: 6px; margin-bottom: 12px; font-size: 12px; }
.vault-income-icon { font-size: 14px; }
.vault-income-label { color: #888; }
.vault-income-value { color: #8bc34a; font-weight: bold; }
.vault-income-sep { color: #444; margin: 0 4px; }
.vault-income-bank { color: var(--hf-accent, #caa14a); font-size: 11px; }
/* --- BLOCK PROGRESS INDICATOR CSS --- */
/* Block Icon Styles */
.vault-block-icon { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; cursor: pointer; transition: all 0.2s; margin: 0 2px; }
.vault-block-icon .block-icon-shape { font-size: 18px; transition: all 0.2s; }
.vault-block-icon .block-tier-num { position: absolute; font-size: 8px; font-weight: bold; color: #fff; text-shadow: 0 0 2px #000; }
.vault-block-icon.no-block .block-icon-shape { color: #444; opacity: 0.5; }
.vault-block-icon.has-tier .block-icon-shape { color: var(--hf-accent, #caa14a); opacity: 1; }
.vault-block-icon.has-tier.close .block-icon-shape { color: #ffd54f; }
.vault-block-icon.close .block-icon-shape { color: #ffd54f; opacity: 0.8; }
.vault-block-icon.can-afford .block-icon-shape { color: #8bc34a; animation: blockIconPulse 1.5s ease-in-out infinite; }
.vault-block-icon.maxed .block-icon-shape { color: #8bc34a; opacity: 1; }
@keyframes blockIconPulse { 0%, 100% { filter: drop-shadow(0 0 4px rgba(139, 195, 74, 0.6)); transform: scale(1); } 50% { filter: drop-shadow(0 0 8px rgba(139, 195, 74, 0.9)); transform: scale(1.1); } }
.vault-block-icon:hover .block-icon-shape { transform: scale(1.15); }
.vault-block-progress { height: 3px; background: #252525; border-radius: 2px; margin: 2px 0 4px 0; overflow: hidden; }
.vault-block-progress-fill { height: 100%; background: linear-gradient(90deg, #444, var(--hf-accent, #caa14a)); border-radius: 2px; transition: width 0.3s; }
.vault-block-progress-fill.close { background: linear-gradient(90deg, #ffd54f, #ffb300); }
/* --- QUICK BLOCK MODAL CSS --- */
.quick-block-container { display: flex; flex-direction: column; gap: 15px; }
.quick-block-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 10px; border-bottom: 1px solid #333; }
.quick-block-symbol { font-size: 24px; font-weight: bold; color: #fff; }
.quick-block-type { font-size: 11px; color: #888; text-transform: uppercase; background: #252525; padding: 4px 8px; border-radius: 4px; }
.quick-block-status { display: flex; align-items: center; justify-content: center; gap: 20px; padding: 15px; background: #1a1a1a; border-radius: 8px; }
.quick-block-current, .quick-block-next { text-align: center; }
.quick-block-label { display: block; font-size: 10px; color: #666; text-transform: uppercase; margin-bottom: 4px; }
.quick-block-value { font-size: 16px; font-weight: bold; color: var(--hf-accent, #caa14a); }
.quick-block-value.maxed { color: #8bc34a; }
.quick-block-arrow { font-size: 20px; color: #444; }
.quick-block-progress-section { background: #151515; padding: 12px; border-radius: 6px; }
.quick-block-progress-header { display: flex; justify-content: space-between; font-size: 11px; color: #888; margin-bottom: 8px; }
.quick-block-progress-bar { height: 8px; background: #252525; border-radius: 4px; overflow: hidden; }
.quick-block-progress-fill { height: 100%; background: linear-gradient(90deg, var(--hf-accent, #caa14a), #4a7a7a); border-radius: 4px; transition: width 0.3s; }
.quick-block-progress-fill.close { background: linear-gradient(90deg, #ffd54f, #ffb300); }
.quick-block-shares { text-align: center; font-size: 11px; color: #666; margin-top: 8px; }
.quick-block-details { background: #1a1a1a; padding: 12px; border-radius: 6px; }
.quick-block-detail-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 12px; color: #ccc; border-bottom: 1px solid #252525; }
.quick-block-detail-row:last-child { border-bottom: none; }
.quick-block-detail-row.highlight { background: #1a2a1a; margin: 0 -12px; padding: 8px 12px; font-weight: bold; color: #fff; }
.quick-block-afford { text-align: center; font-size: 12px; padding: 10px; background: #151515; border-radius: 6px; }
.quick-block-actions { display: flex; gap: 10px; margin-top: 5px; }
.qb-btn-buy { flex: 1; border-color: #8bc34a !important; color: #8bc34a !important; }
.qb-btn-buy:disabled { border-color: #444 !important; color: #666 !important; }
.qb-btn-rebalance { flex: 1; border-color: #ffd54f !important; color: #ffd54f !important; }
.qb-btn-rebalance:disabled { border-color: #444 !important; color: #666 !important; }
.qb-btn-close { flex: 0 0 80px; background: #333 !important; border-color: #555 !important; color: #888 !important; }
.quick-block-rebalance-info { background: linear-gradient(135deg, #2a2a1a 0%, #1a1a15 100%); border: 1px solid #554422; border-radius: 6px; padding: 12px; }
.qb-rebalance-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 12px; font-weight: bold; color: #ffd54f; }
.qb-rebalance-total { font-size: 11px; color: #fff; background: #332211; padding: 2px 8px; border-radius: 4px; }
.qb-rebalance-detail { font-size: 10px; color: #888; margin-bottom: 8px; }
.qb-rebalance-funds { display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 11px; color: #ccc; background: #1a1a1a; padding: 8px; border-radius: 4px; }
.alfa-vault-status { text-align: center; font-size: 11px; padding: 5px 0; min-height: 18px; }
/* --- VAULT CONFIG MODAL CSS --- */
.vault-config-container { display: flex; flex-direction: column; gap: 15px; }
.vault-config-section { background: #1a1a1a; border-radius: 6px; padding: 12px; }
.vault-config-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.vault-config-title { font-size: 11px; font-weight: bold; color: #888; text-transform: uppercase; }
.vault-stock-count { font-size: 11px; color: var(--hf-accent, #caa14a); }
.vault-stock-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; max-height: 200px; overflow-y: auto; padding: 5px; background: #111; border-radius: 4px; }
.vault-stock-option { display: flex; align-items: center; gap: 5px; padding: 5px 8px; background: #1a1a1a; border: 1px solid #333; border-radius: 4px; cursor: pointer; transition: all 0.15s; }
.vault-stock-option:hover { border-color: #555; }
.vault-stock-option.selected { border-color: var(--hf-accent, #caa14a); background: var(--hf-accent, #caa14a)15; }
.vault-stock-option input { display: none; }
.vault-stock-sym { font-weight: bold; color: #fff; font-size: 11px; }
.vault-stock-type { font-size: 9px; color: #666; }
.vault-quick-actions { display: flex; gap: 8px; margin-top: 10px; justify-content: flex-end; }
.vault-config-row { display: flex; gap: 15px; }
.vault-strategy-desc { font-size: 10px; color: #666; margin-top: 6px; line-height: 1.3; }
.vault-config-actions { display: flex; gap: 10px; padding-top: 10px; border-top: 1px solid #333; }
/* --- VAULT ACTION BUTTONS CSS --- */
.vault-stock-actions { display: flex; gap: 4px; margin-left: auto; }
.vault-action-btn { background: transparent; border: 1px solid #444; color: #888; font-size: 9px; padding: 2px 6px; border-radius: 3px; cursor: pointer; transition: all 0.15s; }
.vault-action-btn:hover:not(:disabled) { border-color: var(--hf-accent, #caa14a); color: #fff; }
.vault-action-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.vault-quick-buy:hover:not(:disabled) { border-color: #66bb6a; color: #66bb6a; }
.vault-quick-sell:hover:not(:disabled) { border-color: #ef5350; color: #ef5350; }
.vault-toggle-lock { font-size: 11px; padding: 1px 5px; }
.vault-toggle-lock.locked { border-color: #ffd54f; background: #ffd54f22; }
.vault-row-locked { border-color: #ffd54f44 !important; background: linear-gradient(90deg, #ffd54f08 0%, #1a1a1a 30%) !important; }
.vault-row-locked .vault-stock-name::after { content: ' \u{1F512}'; font-size: 9px; }
/* --- ANALYZE & REBALANCE BUTTON CSS --- */
.vault-analyze-btn { border-color: var(--hf-accent, #caa14a) !important; color: var(--hf-accent, #caa14a) !important; }
.vault-analyze-btn:hover { background: var(--hf-accent, #caa14a) !important; color: #fff !important; }
.vault-rebalance-btn { border-color: var(--hf-accent, #caa14a) !important; color: var(--hf-accent, #caa14a) !important; }
.vault-rebalance-btn:hover { background: var(--hf-accent, #caa14a) !important; color: #fff !important; }
.rebal-container { display: flex; flex-direction: column; gap: 15px; }
.rebal-summary { display: flex; justify-content: space-around; background: #1a1a1a; padding: 12px; border-radius: 6px; }
.rebal-summary-item { text-align: center; }
.rebal-summary-label { display: block; font-size: 10px; color: #888; text-transform: uppercase; margin-bottom: 4px; }
.rebal-summary-value { font-size: 14px; font-weight: bold; color: #fff; text-transform: capitalize; }
.rebal-section { background: #151515; border-radius: 6px; padding: 10px; }
.rebal-section-header { font-size: 12px; font-weight: bold; margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid #333; }
.rebal-table { display: flex; flex-direction: column; gap: 4px; max-height: 150px; overflow-y: auto; }
.rebal-row { display: flex; align-items: center; padding: 4px 8px; background: #1a1a1a; border-radius: 4px; font-size: 11px; gap: 8px; }
.rebal-row-executing { background: #2a2a1a; border: 1px solid #554422; }
.rebal-row-completed { background: #1a2a1a; opacity: 0.7; }
.rebal-row-failed { background: #2a1a1a; border: 1px solid #554422; }
.rebal-action-btn { background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; padding: 2px 10px; font-size: 10px; font-weight: bold; cursor: pointer; transition: all 0.2s; min-width: 50px; }
.rebal-action-btn:hover:not(:disabled) { background: #444; border-color: var(--hf-accent, #caa14a); }
.rebal-action-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.rebal-action-btn[data-type="sell"] { border-color: #ef5350; color: #ef5350; }
.rebal-action-btn[data-type="sell"]:hover:not(:disabled) { background: #ef5350; color: #fff; }
.rebal-action-btn[data-type="buy"] { border-color: #66bb6a; color: #66bb6a; }
.rebal-action-btn[data-type="buy"]:hover:not(:disabled) { background: #66bb6a; color: #fff; }
.rebal-sym { font-weight: bold; color: #fff; min-width: 40px; }
.rebal-signal { font-size: 9px; min-width: 55px; text-transform: uppercase; }
.rebal-reason { font-size: 9px; color: #666; flex: 1; }
.rebal-shares { min-width: 70px; text-align: right; color: #ccc; }
.rebal-value { min-width: 60px; text-align: right; font-weight: bold; }
.rebal-subtotal { text-align: right; font-size: 12px; font-weight: bold; margin-top: 8px; padding-top: 8px; border-top: 1px dashed #333; }
.rebal-warning { font-size: 10px; color: #888; text-align: center; padding: 8px; background: #1a1a1a; border-radius: 4px; }
.rebal-actions { display: flex; gap: 10px; padding-top: 10px; border-top: 1px solid #333; }
/* --- PORTFOLIO P&L CSS --- */
.alfa-vault-pl-section { background: #151515; border: 1px solid #333; border-radius: 6px; margin-bottom: 12px; overflow: hidden; }
.vault-pl-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; cursor: pointer; transition: background 0.2s; }
.vault-pl-header:hover { background: #1a1a1a; }
.vault-pl-title { font-size: 12px; font-weight: bold; color: var(--hf-accent, #caa14a); }
.vault-pl-content { padding: 0 12px 12px; }
.vault-pl-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 10px; }
.vault-pl-item { background: #1a1a1a; padding: 10px; border-radius: 6px; text-align: center; }
.vault-pl-item.pl-positive { border-left: 3px solid #8bc34a; }
.vault-pl-item.pl-negative { border-left: 3px solid #ef5350; }
.vault-pl-label { display: block; font-size: 10px; color: #888; text-transform: uppercase; margin-bottom: 4px; }
.vault-pl-value { display: block; font-size: 13px; font-weight: bold; color: #fff; }
.vault-pl-item.pl-positive .vault-pl-value { color: #8bc34a; }
.vault-pl-item.pl-negative .vault-pl-value { color: #ef5350; }
.vault-pl-actions { display: flex; gap: 8px; align-items: center; padding-top: 10px; border-top: 1px solid #333; }
.vault-pl-sync-info { font-size: 10px; color: #666; margin-left: auto; }
.vault-pl-empty { text-align: center; padding: 20px; color: #888; }
.vault-stock-pl { font-size: 10px; font-weight: bold; margin-left: 4px; }
.vault-pl-warning { background: #ff980022; border: 1px solid #ff980055; border-radius: 6px; padding: 10px; margin-bottom: 12px; font-size: 11px; color: #ffb74d; line-height: 1.4; }
/* Period Filter Buttons */
.vault-pl-period-filters { display: flex; gap: 5px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
.pl-period-btn { background: transparent; border: 1px solid #444; color: #888; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 11px; transition: all 0.2s; }
.pl-period-btn:hover { border-color: var(--hf-accent, #caa14a); color: #fff; }
.pl-period-btn.active { background: var(--hf-accent, #caa14a); border-color: var(--hf-accent, #caa14a); color: #fff; }
/* Period Summary */
.vault-pl-period-summary { background: #1a1a1a; border-radius: 6px; padding: 12px; margin-bottom: 12px; }
.pl-period-title { font-size: 12px; font-weight: bold; color: var(--hf-accent, #caa14a); margin-bottom: 10px; }
.pl-period-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.pl-period-stat { display: flex; justify-content: space-between; align-items: center; padding: 6px 8px; background: #252525; border-radius: 4px; }
.pl-period-stat.pl-positive { border-left: 2px solid #8bc34a; }
.pl-period-stat.pl-negative { border-left: 2px solid #ef5350; }
.pl-stat-label { font-size: 10px; color: #888; text-transform: uppercase; }
.pl-stat-value { font-size: 12px; font-weight: bold; color: #fff; }
.pl-period-stat.pl-positive .pl-stat-value { color: #8bc34a; }
.pl-period-stat.pl-negative .pl-stat-value { color: #ef5350; }
.vault-pl-divider { border-top: 1px solid #333; margin: 12px 0; }
.vault-pl-section-title { font-size: 11px; color: #888; text-transform: uppercase; margin-bottom: 8px; font-weight: bold; }
/* --- GAMBLE MODULE CSS --- */
.alfa-gamble-section { background: #151515; border: 1px solid #333; border-radius: 6px; margin: 12px 0; }
.gamble-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; cursor: pointer; transition: background 0.2s; gap: 10px; }
.gamble-header:hover { background: #1a1a1a; }
.gamble-title { font-weight: bold; color: var(--hf-accent, #caa14a); font-size: 12px; }
.gamble-content { padding: 0 12px 12px; }
.gamble-target-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
.gamble-target-label { font-size: 11px; color: #888; }
.gamble-target-select { flex: 1; min-width: 80px; max-width: 120px; }
.gamble-cash-row { font-size: 12px; color: #aaa; margin-bottom: 10px; }
.gamble-cash-label { margin-right: 6px; }
.gamble-cash-value { font-weight: bold; color: #8bc34a; }
.gamble-presets-row { display: flex; gap: 20px; margin-bottom: 12px; flex-wrap: wrap; }
.gamble-presets-group { flex: 1; min-width: 180px; }
.gamble-presets-label { font-size: 10px; color: #888; text-transform: uppercase; margin-bottom: 6px; }
.gamble-presets-btns { display: flex; flex-wrap: wrap; gap: 6px; }
.gamble-preset-btn { background: transparent; border: 1px solid #555; color: #888; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 11px; transition: all 0.2s; }
.gamble-preset-btn:hover { border-color: var(--hf-accent, #caa14a); color: #fff; }
.gamble-preset-btn.gamble-deposit:hover { border-color: #66bb6a; color: #66bb6a; }
.gamble-preset-btn.gamble-withdraw:hover { border-color: #ef5350; color: #ef5350; }
.gamble-panic-row { margin-top: 10px; padding-top: 10px; border-top: 1px solid #333; }
.gamble-panic-btn { width: 100%; background: #ef535022; border: 1px solid #ef5350; color: #ef5350; padding: 10px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: bold; transition: all 0.2s; }
.gamble-panic-btn:hover { background: #ef5350; color: #fff; }
.gamble-status { font-size: 11px; margin-top: 8px; min-height: 20px; }
.gamble-config-modal { display: flex; flex-direction: column; gap: 15px; }
.gamble-config-section { display: flex; flex-direction: column; gap: 8px; }
.gamble-config-title { font-size: 12px; font-weight: bold; color: var(--hf-accent, #caa14a); }
.gamble-config-row { display: flex; gap: 10px; align-items: center; }
.gamble-config-hint { font-size: 10px; color: #666; }
.gamble-config-actions { display: flex; gap: 10px; }
/* --- SWING TRADING CSS --- */
.alfa-swing-section { background: #151515; border: 1px solid #333; border-radius: 6px; margin: 12px 0; }
.swing-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; cursor: pointer; transition: background 0.2s; }
.swing-header:hover { background: #1a1a1a; }
.swing-title { font-weight: bold; color: var(--hf-accent, #caa14a); font-size: 12px; }
.swing-content { padding: 0 12px 12px; overflow-x: hidden; }
.stocks-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; cursor: pointer; transition: background 0.2s; }
.stocks-header:hover { background: #1a1a1a; }
.stocks-title { font-weight: bold; color: var(--hf-accent, #caa14a); font-size: 12px; }
.stocks-content { padding: 0 12px 12px; overflow-x: hidden; }
.alfa-stocks-section { background: #151515; border: 1px solid #333; border-radius: 6px; margin: 12px 0; overflow: hidden; }
.swing-tabs { display: flex; gap: 5px; margin-bottom: 12px; border-bottom: 1px solid #333; padding-bottom: 10px; }
.swing-tab { background: transparent; border: 1px solid #444; color: #888; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 11px; transition: all 0.2s; }
.swing-tab:hover { border-color: var(--hf-accent, #caa14a); color: #fff; }
.swing-tab.active { background: var(--hf-accent, #caa14a); border-color: var(--hf-accent, #caa14a); color: #fff; }
.swing-tab-content { min-height: 100px; overflow-x: hidden; }
.swing-empty { text-align: center; padding: 30px 15px; color: #666; }
.swing-loading { text-align: center; padding: 20px; color: #888; }
.swing-error { text-align: center; padding: 20px; color: #ef5350; }
.swing-actions { display: flex; gap: 10px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #333; }
/* Positions Summary */
.swing-positions-summary { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
.swing-summary-item { background: #252525; padding: 10px 15px; border-radius: 6px; text-align: center; flex: 1; min-width: 100px; }
.swing-summary-item.positive { border-left: 3px solid #8bc34a; }
.swing-summary-item.negative { border-left: 3px solid #ef5350; }
.swing-summary-label { display: block; font-size: 10px; color: #888; text-transform: uppercase; margin-bottom: 4px; }
.swing-summary-value { font-size: 14px; font-weight: bold; color: #fff; }
.swing-summary-item.positive .swing-summary-value { color: #8bc34a; }
.swing-summary-item.negative .swing-summary-value { color: #ef5350; }
/* Position Cards */
.swing-position { background: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 12px; margin-bottom: 10px; transition: all 0.2s; }
.swing-position:hover { border-color: #555; }
.swing-position.target-hit { border-color: #8bc34a; background: #8bc34a11; }
.swing-position.stopped { border-color: #ef5350; background: #ef535011; }
.swing-pos-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.swing-pos-title-group { display: flex; align-items: center; gap: 8px; }
.swing-pos-header-right { display: flex; align-items: center; gap: 8px; }
.swing-pos-symbol { font-size: 16px; font-weight: bold; color: #fff; }
.swing-pos-signal { font-size: 9px; font-weight: bold; padding: 2px 6px; border-radius: 4px; white-space: nowrap; }
.swing-signal-stale { opacity: 0.6; }
.swing-pos-pl { font-size: 14px; font-weight: bold; }
.swing-pos-pl.positive { color: #8bc34a; }
.swing-pos-pl.negative { color: #ef5350; }
.swing-pos-status { font-size: 16px; }
.swing-pos-details { font-size: 11px; color: #aaa; }
.swing-pos-row { display: flex; justify-content: space-between; margin-bottom: 6px; flex-wrap: wrap; gap: 5px; }
.swing-stop { color: #ef5350; }
.swing-target { color: #8bc34a; }
.swing-pos-progress { margin: 10px 0; }
.swing-progress-bar { height: 6px; background: linear-gradient(to right, #ef5350, #ffd54f 50%, #8bc34a); border-radius: 3px; position: relative; }
.swing-progress-fill { position: absolute; left: 0; top: 0; height: 100%; background: rgba(0,0,0,0.5); border-radius: 3px 0 0 3px; }
.swing-progress-marker { position: absolute; top: -3px; width: 4px; height: 12px; background: #fff; border-radius: 2px; transform: translateX(-50%); box-shadow: 0 0 5px rgba(0,0,0,0.5); }
.swing-progress-labels { display: flex; justify-content: space-between; font-size: 9px; color: #666; margin-top: 3px; }
.swing-pos-meta { display: flex; justify-content: space-between; font-size: 10px; color: #666; margin-top: 8px; }
.swing-pos-actions { display: flex; gap: 8px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #333; }
.swing-close-btn, .swing-merge-btn, .swing-edit-btn { background: transparent; border: 1px solid #555; color: #888; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 10px; transition: all 0.2s; }
.swing-close-btn:hover { border-color: #ef5350; color: #ef5350; }
.swing-merge-btn:hover { border-color: var(--hf-accent, #caa14a); color: var(--hf-accent, #caa14a); }
.swing-edit-btn:hover { border-color: var(--hf-accent, #caa14a); color: var(--hf-accent, #caa14a); }
/* Signals */
.swing-signal-group { margin-bottom: 15px; }
.swing-signal-group-title { font-size: 12px; font-weight: bold; margin-bottom: 8px; padding-bottom: 5px; border-bottom: 1px solid #333; }
.swing-signal-card { background: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 12px; margin-bottom: 8px; }
.swing-signal-card.buy { border-left: 3px solid #66bb6a; }
.swing-signal-card.sell { border-left: 3px solid #ef5350; }
.swing-signal-header { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; flex-wrap: wrap; }
.swing-signal-symbol { font-size: 14px; font-weight: bold; color: #fff; }
.swing-signal-badge { padding: 3px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; }
.swing-signal-score { font-size: 10px; color: #888; margin-left: auto; }
.swing-signal-price { font-size: 18px; font-weight: bold; color: #fff; margin-bottom: 8px; }
.swing-signal-reasons { display: flex; flex-direction: column; gap: 3px; margin-bottom: 8px; }
.swing-reason { font-size: 10px; padding: 3px 8px; border-radius: 4px; }
.swing-reason.bullish { background: #66bb6a22; color: #66bb6a; }
.swing-reason.bearish { background: #ef535022; color: #ef5350; }
.swing-signal-targets { display: flex; justify-content: space-between; font-size: 10px; color: #888; margin-bottom: 8px; }
.swing-signal-holding { font-size: 10px; color: #ffd54f; background: #ffd54f22; padding: 5px 10px; border-radius: 4px; margin-top: 5px; }
.swing-signal-actions { display: flex; gap: 8px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
.swing-signal-actions .swing-signal-track-btn { flex: 1; min-width: 120px; }
.swing-signal-actions .swing-already-tracking { flex: 1; }
.swing-signal-track-btn { background: #66bb6a; border: none; color: #fff; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: bold; }
.swing-signal-track-btn:hover { background: #4caf50; }
.swing-already-tracking { font-size: 10px; color: #888; font-style: italic; }
/* Stats */
.swing-stats { margin-bottom: 15px; }
.swing-stats-empty { text-align: center; padding: 15px; color: #666; font-size: 11px; }
.swing-stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.swing-stat-card { background: #1a1a1a; padding: 12px; border-radius: 6px; text-align: center; }
.swing-stat-card.positive { border-bottom: 2px solid #8bc34a; }
.swing-stat-card.negative { border-bottom: 2px solid #ef5350; }
.swing-stat-label { display: block; font-size: 10px; color: #888; text-transform: uppercase; margin-bottom: 4px; }
.swing-stat-value { font-size: 16px; font-weight: bold; color: #fff; }
.swing-stat-value.positive { color: #8bc34a; }
.swing-stat-value.negative { color: #ef5350; }
.swing-stat-sub { display: block; font-size: 10px; color: #666; margin-top: 3px; }
/* Journal */
.swing-journal-list { max-height: 400px; overflow-y: auto; }
.swing-journal-entry { background: #1a1a1a; border: 1px solid #333; border-radius: 6px; padding: 10px; margin-bottom: 8px; }
.swing-journal-entry.winner { border-left: 3px solid #8bc34a; }
.swing-journal-entry.loser { border-left: 3px solid #ef5350; }
.swing-journal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; }
.swing-journal-symbol { font-size: 13px; font-weight: bold; color: #fff; }
.swing-journal-pnl { font-size: 12px; font-weight: bold; }
.swing-journal-pnl.positive { color: #8bc34a; }
.swing-journal-pnl.negative { color: #ef5350; }
.swing-journal-details { display: flex; justify-content: space-between; font-size: 10px; color: #aaa; margin-bottom: 3px; }
.swing-journal-meta { display: flex; justify-content: space-between; font-size: 9px; color: #666; flex-wrap: wrap; gap: 5px; }
.swing-journal-header { position: relative; }
.swing-journal-header-right { display: flex; align-items: center; gap: 8px; }
.swing-journal-actions { display: flex; gap: 4px; }
.swing-journal-edit-btn, .swing-journal-full-edit-btn, .swing-journal-delete-btn { background: transparent; border: 1px solid #444; color: #888; padding: 2px 6px; border-radius: 4px; cursor: pointer; font-size: 11px; transition: all 0.2s; }
.swing-journal-edit-btn:hover, .swing-journal-full-edit-btn:hover { border-color: var(--hf-accent, #caa14a); color: var(--hf-accent, #caa14a); }
.swing-journal-delete-btn:hover { border-color: #ef5350; color: #ef5350; }
.swing-journal-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px; }
.swing-journal-tag { background: #2a2a2a; color: var(--hf-accent, #caa14a); padding: 2px 8px; border-radius: 4px; font-size: 9px; }
.swing-journal-notes-preview { font-size: 10px; color: #888; margin-top: 4px; font-style: italic; }
.swing-journal-entry-editing .swing-journal-inline-edit { display: block; }
.swing-journal-inline-edit { display: flex; flex-direction: column; gap: 8px; }
.swing-journal-inline-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.swing-journal-inline-fields { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.swing-journal-inline-fields label { font-size: 10px; color: #888; }
.swing-inline-input { width: 70px; background: #1a1a1a; border: 1px solid #333; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; }
.swing-journal-inline-actions { display: flex; gap: 8px; }
.swing-undo-banner { background: #2a3a2a; border: 1px solid #66bb6a; border-radius: 6px; padding: 10px 15px; margin-bottom: 10px; font-size: 12px; color: #8bc34a; display: flex; align-items: center; gap: 10px; }
.swing-undo-btn { background: #66bb6a; border: none; color: #fff; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: bold; }
.swing-undo-btn:hover { background: #7bc34a; }
.pos-merge-notice { background: #2a2a1a; border: 1px solid var(--hf-accent, #caa14a); border-radius: 6px; padding: 10px; margin-bottom: 10px; font-size: 12px; color: #aaa; }
.pos-merge-buttons { display: flex; gap: 8px; margin-top: 8px; }
.merge-existing-info { font-size: 11px; color: #888; margin-top: 5px; }
.merge-shares-context { background: #1a2a2a; border: 1px solid #333; border-radius: 6px; padding: 10px; font-size: 12px; color: #aaa; }
.merge-shares-context span { display: block; margin-bottom: 4px; }
.merge-shares-context span:last-child { margin-bottom: 0; }
.merge-no-loose { background: #2a2a1a; border: 1px solid #554422; border-radius: 6px; padding: 10px; font-size: 12px; color: #ffd54f; }
.merge-result-value { font-size: 13px; font-weight: bold; color: var(--hf-accent, #caa14a); margin-top: 5px; }
/* Add Position Modal */
.add-position-form { display: flex; flex-direction: column; gap: 15px; }
.form-group { display: flex; flex-direction: column; gap: 5px; }
.form-group label { font-size: 11px; color: #888; text-transform: uppercase; }
.form-row { display: flex; gap: 15px; }
.form-row .form-group { flex: 1; }
.form-hint { font-size: 10px; color: #666; }
.form-summary { background: #1a1a1a; padding: 15px; border-radius: 6px; }
.summary-row { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 12px; }
.summary-row:last-child { margin-bottom: 0; }
.summary-row span:first-child { color: #888; }
.summary-row span:last-child { font-weight: bold; color: #fff; }
.summary-row .positive { color: #8bc34a !important; }
.summary-row .negative { color: #ef5350 !important; }
.form-actions { display: flex; gap: 10px; }
.alfa-select { background: #1a1a1a; border: 1px solid #333; color: #fff; padding: 10px; border-radius: 6px; font-size: 13px; width: 100%; min-height: 40px; line-height: 1.4; box-sizing: border-box; }
.alfa-select:focus { border-color: var(--hf-accent, #caa14a); outline: none; }
/* --- TRANSACTION HISTORY MODAL CSS --- */
.tx-history-container { display: flex; flex-direction: column; gap: 15px; }
.tx-period-filter-row { display: flex; align-items: center; gap: 8px; padding-bottom: 10px; border-bottom: 1px solid #333; flex-wrap: wrap; }
.tx-period-label { font-size: 11px; color: #888; }
.tx-period-btn { background: transparent; border: 1px solid #444; color: #888; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 10px; transition: all 0.2s; }
.tx-period-btn:hover { border-color: var(--hf-accent, #caa14a); color: #fff; }
.tx-period-btn.active { background: var(--hf-accent, #caa14a); border-color: var(--hf-accent, #caa14a); color: #fff; }
.tx-period-badge { display: inline-block; background: var(--hf-accent, #caa14a)33; color: var(--hf-accent, #caa14a); padding: 4px 12px; border-radius: 12px; font-size: 11px; font-weight: bold; margin-bottom: 10px; }
.tx-tabs { display: flex; gap: 5px; border-bottom: 1px solid #333; padding-bottom: 10px; }
.tx-tab { background: transparent; border: 1px solid #444; color: #888; padding: 6px 15px; border-radius: 4px; cursor: pointer; font-size: 11px; transition: all 0.2s; }
.tx-tab:hover { border-color: var(--hf-accent, #caa14a); color: #fff; }
.tx-tab.active { background: var(--hf-accent, #caa14a); border-color: var(--hf-accent, #caa14a); color: #fff; }
.tx-tab-content { min-height: 200px; }
.tx-summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.tx-summary-card { background: #1a1a1a; padding: 15px; border-radius: 8px; text-align: center; border: 1px solid #252525; }
.tx-summary-card.pl-positive { border-color: #8bc34a44; }
.tx-summary-card.pl-negative { border-color: #ef535044; }
.tx-summary-label { display: block; font-size: 10px; color: #888; text-transform: uppercase; margin-bottom: 6px; }
.tx-summary-value { display: block; font-size: 18px; font-weight: bold; color: #fff; }
.tx-summary-card.pl-positive .tx-summary-value { color: #8bc34a; }
.tx-summary-card.pl-negative .tx-summary-value { color: #ef5350; }
.tx-summary-sub { display: block; font-size: 11px; color: #666; margin-top: 4px; }
.tx-table { width: 100%; border-collapse: collapse; font-size: 11px; }
.tx-table th { text-align: left; padding: 8px; color: #888; border-bottom: 1px solid #333; font-weight: bold; text-transform: uppercase; font-size: 10px; position: sticky; top: 0; background: #1e1e1e; }
.tx-table td { padding: 8px; border-bottom: 1px solid #252525; }
.tx-table tbody tr:hover { background: #252525; }
.tx-actions { display: flex; gap: 10px; padding-top: 15px; border-top: 1px solid #333; }
/* --- QUICK BUY/SELL MODAL CSS --- */
.vault-trade-view { padding: 12px 0; }
.vault-trade-view-inner { display: flex; flex-direction: column; gap: 15px; }
.vault-trade-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 10px; border-bottom: 1px solid #333; }
.vault-trade-symbol { font-size: 20px; font-weight: bold; color: #fff; }
.vault-trade-price { font-size: 16px; color: #8bc34a; }
.quick-trade-container { display: flex; flex-direction: column; gap: 15px; }
.quick-trade-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 10px; border-bottom: 1px solid #333; }
.quick-trade-symbol { font-size: 24px; font-weight: bold; color: #fff; }
.quick-trade-price { font-size: 16px; font-weight: bold; color: var(--hf-accent, #caa14a); }
.quick-trade-info { background: #1a1a1a; padding: 12px; border-radius: 6px; }
.quick-trade-info-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 12px; color: #ccc; border-bottom: 1px solid #252525; }
.quick-trade-info-row:last-child { border-bottom: none; }
.quick-trade-protection { background: #2a2a1a; border: 1px solid #554422; border-radius: 6px; padding: 10px; margin-top: 5px; }
.quick-trade-protection-item { font-size: 11px; color: #ffd54f; margin-bottom: 5px; }
.quick-trade-protection-item:last-child { margin-bottom: 0; }
.quick-trade-input-section { display: flex; flex-direction: column; gap: 8px; }
.quick-trade-label { font-size: 11px; color: #888; text-transform: uppercase; }
.quick-trade-hint { font-size: 10px; color: #666; }
.quick-trade-calc { background: #151515; padding: 12px; border-radius: 6px; margin-top: 5px; }
.quick-trade-calc-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 12px; color: #ccc; border-bottom: 1px solid #252525; }
.quick-trade-calc-row:last-child { border-bottom: none; }
.quick-trade-calc-row.highlight { background: #1a2a1a; margin: 0 -12px; padding: 8px 12px; font-weight: bold; color: #fff; }
.quick-trade-warning { margin-top: 8px; padding: 8px; background: #2a1a1a; border-radius: 4px; font-size: 11px; }
.quick-trade-actions { display: flex; gap: 10px; margin-top: 5px; }
.qb-btn-sell { flex: 1; border-color: #ef5350 !important; color: #ef5350 !important; }
.qb-btn-sell:hover:not(:disabled) { background: #ef5350 !important; color: #fff !important; }
.qb-btn-sell:disabled { border-color: #444 !important; color: #666 !important; }
`;
// src/stocks/styles.js
var STYLE_ID2 = "hf-stocks-styles";
function injectStocksStyles() {
if (document.getElementById(STYLE_ID2)) {
return;
}
const style = document.createElement("style");
style.id = STYLE_ID2;
style.textContent = `
${LEGACY_STYLES}
.hf-stocks-root {
font-family: var(--hf-font);
color: var(--hf-text);
}
.hf-stocks-root,
.alfa-modal-overlay.hf-torn {
--hf-stock-card-bg: rgba(21, 22, 26, 0.92);
--hf-stock-card-bg-strong: rgba(27, 29, 34, 0.96);
--hf-stock-card-hover: rgba(202, 161, 74, 0.08);
--hf-stock-border-soft: rgba(202, 161, 74, 0.16);
}
.hf-stocks-root .alfa-vault-section {
background: transparent;
border: none;
padding: 0;
margin-bottom: 0;
font-family: var(--hf-font);
color: var(--hf-text);
}
/* Space content away from collapsible module header border */
.hf-stocks-root .vault-pl-content,
.hf-stocks-root .gamble-content,
.hf-stocks-root .swing-content,
.hf-stocks-root .stocks-content {
padding-top: 12px;
}
.hf-stocks-root .alfa-vault-empty,
.hf-stocks-root .alfa-vault-summary,
.hf-stocks-root .alfa-vault-income-row,
.hf-stocks-root .alfa-vault-pl-section,
.hf-stocks-root .alfa-gamble-section,
.hf-stocks-root .alfa-swing-section,
.hf-stocks-root .alfa-stocks-section,
.hf-stocks-root .vault-stock-row,
.hf-stocks-root .swing-position,
.hf-stocks-root .swing-signal-card,
.hf-stocks-root .swing-journal-entry,
.hf-stocks-root .tx-summary-card,
.hf-stocks-root .vault-pl-item,
.hf-stocks-root .swing-summary-item,
.hf-stocks-root .swing-stat-card,
.hf-stocks-root .rebal-section,
.hf-stocks-root .rebal-summary,
.hf-stocks-root .quick-trade-info,
.hf-stocks-root .quick-trade-calc {
background: var(--hf-stock-card-bg);
border: 1px solid var(--hf-panel-border);
border-radius: var(--hf-radius);
color: var(--hf-text);
box-shadow: none;
}
.hf-stocks-root .alfa-vault-summary,
.hf-stocks-root .alfa-vault-income-row {
background: linear-gradient(180deg, rgba(32, 34, 40, 0.96), rgba(21, 22, 26, 0.96));
border-color: var(--hf-stock-border-soft);
}
.hf-stocks-root .alfa-vault-total-value,
.hf-stocks-root .vault-stock-value,
.hf-stocks-root .vault-income-value,
.hf-stocks-root .gamble-cash-value,
.hf-stocks-root .quick-trade-price,
.hf-stocks-root .vault-trade-price {
color: var(--hf-success);
}
.hf-stocks-root .gamble-presets-group .gamble-cash-row {
margin-bottom: 6px;
}
.hf-stocks-root .gamble-available-value {
font-weight: 700;
color: var(--hf-accent-text);
}
.hf-stocks-root .alfa-vault-total-label,
.hf-stocks-root .vault-pl-label,
.hf-stocks-root .swing-summary-label,
.hf-stocks-root .swing-stat-label,
.hf-stocks-root .quick-block-label,
.hf-stocks-root .quick-trade-label,
.hf-stocks-root .form-group label,
.hf-stocks-root .tx-summary-label,
.hf-stocks-root .pl-stat-label,
.alfa-modal-overlay.hf-torn .form-group label,
.alfa-modal-overlay.hf-torn .settings-section-title,
.alfa-modal-overlay.hf-torn .vault-config-title {
color: var(--hf-text-muted);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
}
.hf-stocks-root .vault-pl-title,
.hf-stocks-root .gamble-title,
.hf-stocks-root .swing-title,
.hf-stocks-root .stocks-title,
.hf-stocks-root .pl-period-title,
.hf-stocks-root .swing-signal-group-title,
.alfa-modal-overlay.hf-torn .settings-section-title,
.alfa-modal-overlay.hf-torn .gamble-config-title,
.alfa-modal-overlay.hf-torn .vault-config-title {
color: var(--hf-accent-text);
}
.hf-stocks-root .vault-pl-header,
.hf-stocks-root .gamble-header,
.hf-stocks-root .swing-header,
.hf-stocks-root .stocks-header {
background: var(--hf-panel-header-bg);
border-bottom: 1px solid var(--hf-panel-border);
}
.hf-stocks-root .alfa-vault-pl-section,
.hf-stocks-root .alfa-gamble-section,
.hf-stocks-root .alfa-swing-section,
.hf-stocks-root .alfa-stocks-section {
overflow: hidden;
}
.hf-stocks-root .vault-pl-header:hover,
.hf-stocks-root .gamble-header:hover,
.hf-stocks-root .swing-header:hover,
.hf-stocks-root .stocks-header:hover,
.hf-stocks-root .vault-stock-row:hover,
.hf-stocks-root .swing-position:hover,
.hf-stocks-root .swing-signal-card:hover,
.hf-stocks-root .swing-journal-entry:hover {
background: var(--hf-stock-card-hover);
border-color: var(--hf-accent-muted);
}
.hf-stocks-root .alfa-vault-header,
.hf-stocks-root .alfa-vault-title {
display: none;
}
.hf-stocks-root .alfa-vault-control-group,
.hf-stocks-root .vault-pl-actions,
.hf-stocks-root .swing-actions,
.hf-stocks-root .quick-block-actions,
.hf-stocks-root .quick-trade-actions,
.alfa-modal-overlay.hf-torn .form-actions,
.alfa-modal-overlay.hf-torn .settings-actions,
.alfa-modal-overlay.hf-torn .vault-config-actions,
.alfa-modal-overlay.hf-torn .gamble-config-actions {
gap: 8px;
}
/* Gamble Presets: Deposit | Withdraw side by side */
.alfa-modal-overlay.hf-torn .gamble-config-modal {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px 20px;
align-items: start;
}
.alfa-modal-overlay.hf-torn .gamble-config-hint,
.alfa-modal-overlay.hf-torn .gamble-config-actions {
grid-column: 1 / -1;
}
.alfa-modal-overlay.hf-torn .gamble-config-row {
width: 100%;
}
.alfa-modal-overlay.hf-torn .gamble-config-label {
width: 64px !important;
flex-shrink: 0;
}
.alfa-modal-overlay.hf-torn .gamble-config-amount {
flex: 1 1 auto !important;
width: auto !important;
min-width: 0;
}
@media (max-width: 560px) {
.alfa-modal-overlay.hf-torn .gamble-config-modal {
grid-template-columns: 1fr;
}
}
.hf-stocks-root .alfa-main-btn,
.hf-stocks-root .alfa-mini-btn,
.hf-stocks-root .alfa-btn,
.hf-stocks-root .pl-period-btn,
.hf-stocks-root .swing-tab,
.hf-stocks-root .tx-tab,
.hf-stocks-root .tx-period-btn,
.hf-stocks-root .gamble-preset-btn,
.hf-stocks-root .vault-action-btn,
.hf-stocks-root .rebal-action-btn,
.hf-stocks-root .swing-close-btn,
.hf-stocks-root .swing-merge-btn,
.hf-stocks-root .swing-edit-btn,
.hf-stocks-root .swing-signal-track-btn,
.hf-stocks-root .swing-journal-edit-btn,
.hf-stocks-root .swing-journal-full-edit-btn,
.hf-stocks-root .swing-journal-delete-btn,
.alfa-modal-overlay.hf-torn .alfa-main-btn,
.alfa-modal-overlay.hf-torn .alfa-mini-btn,
.alfa-modal-overlay.hf-torn .settings-tab,
.alfa-modal-overlay.hf-torn .vault-action-btn,
.alfa-modal-overlay.hf-torn .gamble-preset-btn {
appearance: none;
border: 1px solid var(--hf-panel-border);
border-radius: 5px;
background: var(--hf-surface);
color: var(--hf-accent-text);
cursor: pointer;
font: inherit;
font-size: 11px;
font-weight: 700;
min-height: 28px;
padding: 5px 10px;
text-transform: none;
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, filter 0.15s ease;
}
.hf-stocks-root .alfa-mini-btn,
.alfa-modal-overlay.hf-torn .alfa-mini-btn,
.hf-stocks-root .vault-action-btn {
margin-left: 0;
min-height: 24px;
padding: 3px 8px;
font-size: 10px;
}
.hf-stocks-root .alfa-mini-btn:hover,
.hf-stocks-root .alfa-btn:hover,
.hf-stocks-root .alfa-main-btn:hover,
.hf-stocks-root .pl-period-btn:hover,
.hf-stocks-root .swing-tab:hover,
.hf-stocks-root .tx-tab:hover,
.hf-stocks-root .tx-period-btn:hover,
.hf-stocks-root .gamble-preset-btn:hover,
.hf-stocks-root .vault-action-btn:hover:not(:disabled),
.hf-stocks-root .rebal-action-btn:hover:not(:disabled),
.hf-stocks-root .swing-merge-btn:hover,
.hf-stocks-root .swing-edit-btn:hover,
.alfa-modal-overlay.hf-torn .alfa-main-btn:hover,
.alfa-modal-overlay.hf-torn .alfa-mini-btn:hover,
.alfa-modal-overlay.hf-torn .settings-tab:hover,
.alfa-modal-overlay.hf-torn .vault-action-btn:hover:not(:disabled),
.alfa-modal-overlay.hf-torn .gamble-preset-btn:hover {
background: var(--hf-accent-subtle);
border-color: var(--hf-hover);
color: var(--hf-hover);
}
.hf-stocks-root .alfa-mini-btn.active,
.hf-stocks-root .alfa-btn.active,
.hf-stocks-root .pl-period-btn.active,
.hf-stocks-root .swing-tab.active,
.hf-stocks-root .tx-tab.active,
.hf-stocks-root .tx-period-btn.active,
.alfa-modal-overlay.hf-torn .settings-tab.active {
background: var(--hf-accent);
border-color: var(--hf-accent);
color: #1b1d22;
}
.hf-stocks-root .vault-spread-btn,
.hf-stocks-root .qb-btn-buy,
.hf-stocks-root .swing-signal-track-btn,
.alfa-modal-overlay.hf-torn .qb-btn-buy {
background: var(--hf-success-bg);
border-color: var(--hf-success-border) !important;
color: var(--hf-success) !important;
}
.hf-stocks-root .vault-withdraw-btn,
.hf-stocks-root .qb-btn-sell,
.hf-stocks-root .swing-close-btn,
.hf-stocks-root .swing-journal-delete-btn,
.alfa-modal-overlay.hf-torn .qb-btn-sell {
background: var(--hf-danger-bg);
border-color: var(--hf-danger-border) !important;
color: var(--hf-danger) !important;
}
.hf-stocks-root .vault-rebalance-btn,
.hf-stocks-root .vault-analyze-btn,
.hf-stocks-root .qb-btn-rebalance {
background: var(--hf-warning-bg);
border-color: var(--hf-warning-border) !important;
color: var(--hf-warning) !important;
}
.hf-stocks-root button:disabled,
.alfa-modal-overlay.hf-torn button:disabled {
cursor: not-allowed;
filter: grayscale(0.45);
opacity: 0.55;
}
.hf-stocks-root .alfa-input,
.hf-stocks-root .alfa-select,
.hf-stocks-root .alfa-tbl-input,
.hf-stocks-root .swing-inline-input,
.hf-stocks-root textarea.alfa-input,
.alfa-modal-overlay.hf-torn .alfa-input,
.alfa-modal-overlay.hf-torn .alfa-select,
.alfa-modal-overlay.hf-torn .alfa-tbl-input,
.alfa-modal-overlay.hf-torn .swing-inline-input,
.alfa-modal-overlay.hf-torn textarea.alfa-input {
background: var(--hf-surface);
border: 1px solid var(--hf-panel-border);
border-radius: 5px;
color: var(--hf-accent-text);
font: inherit;
min-height: 30px;
padding: 6px 8px;
}
.hf-stocks-root .alfa-input:focus,
.hf-stocks-root .alfa-select:focus,
.hf-stocks-root .alfa-tbl-input:focus,
.hf-stocks-root .swing-inline-input:focus,
.alfa-modal-overlay.hf-torn .alfa-input:focus,
.alfa-modal-overlay.hf-torn .alfa-select:focus,
.alfa-modal-overlay.hf-torn .alfa-tbl-input:focus,
.alfa-modal-overlay.hf-torn .swing-inline-input:focus {
border-color: var(--hf-accent);
box-shadow: 0 0 0 2px var(--hf-accent-subtle);
outline: none;
}
.hf-stocks-root .vault-stock-bar-container,
.hf-stocks-root .vault-block-progress,
.hf-stocks-root .quick-block-progress-bar {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.04);
}
.hf-stocks-root .vault-stock-bar,
.hf-stocks-root .vault-block-progress-fill,
.hf-stocks-root .quick-block-progress-fill {
background: linear-gradient(90deg, var(--hf-accent), var(--hf-hover));
}
.hf-stocks-root .vault-row-locked {
background: linear-gradient(90deg, rgba(255, 217, 102, 0.08), var(--hf-stock-card-bg) 38%) !important;
border-color: var(--hf-warning-border) !important;
}
.hf-stocks-root .vault-pl-warning,
.hf-stocks-root .settings-warning,
.hf-stocks-root .quick-block-rebalance-info,
.hf-stocks-root .quick-trade-protection,
.hf-stocks-root .merge-no-loose,
.alfa-modal-overlay.hf-torn .settings-warning,
.alfa-modal-overlay.hf-torn .quick-block-rebalance-info,
.alfa-modal-overlay.hf-torn .quick-trade-protection,
.alfa-modal-overlay.hf-torn .merge-no-loose {
background: var(--hf-warning-bg);
border: 1px solid var(--hf-warning-border);
border-radius: var(--hf-radius);
color: var(--hf-warning);
}
.hf-stocks-root .settings-api-status,
.hf-stocks-root .vault-stock-grid,
.hf-stocks-root .settings-exclude-grid,
.hf-stocks-root .settings-checkbox-label,
.hf-stocks-root .vault-stock-option,
.hf-stocks-root .quick-block-status,
.hf-stocks-root .quick-block-progress-section,
.hf-stocks-root .quick-block-details,
.hf-stocks-root .quick-block-afford,
.hf-stocks-root .form-summary,
.alfa-modal-overlay.hf-torn .settings-api-status,
.alfa-modal-overlay.hf-torn .vault-stock-grid,
.alfa-modal-overlay.hf-torn .settings-exclude-grid,
.alfa-modal-overlay.hf-torn .settings-checkbox-label,
.alfa-modal-overlay.hf-torn .vault-stock-option,
.alfa-modal-overlay.hf-torn .quick-block-status,
.alfa-modal-overlay.hf-torn .quick-block-progress-section,
.alfa-modal-overlay.hf-torn .quick-block-details,
.alfa-modal-overlay.hf-torn .quick-block-afford,
.alfa-modal-overlay.hf-torn .form-summary {
background: var(--hf-stock-card-bg);
border: 1px solid var(--hf-panel-border);
border-radius: var(--hf-radius);
}
.hf-stocks-root .vault-stock-option.selected,
.alfa-modal-overlay.hf-torn .vault-stock-option.selected {
background: var(--hf-accent-subtle);
border-color: var(--hf-accent);
}
.hf-stocks-root .vault-stock-name,
.hf-stocks-root .swing-pos-symbol,
.hf-stocks-root .swing-signal-symbol,
.hf-stocks-root .swing-journal-symbol,
.hf-stocks-root .quick-block-symbol,
.hf-stocks-root .vault-trade-symbol,
.hf-stocks-root .quick-trade-symbol,
.alfa-modal-overlay.hf-torn .quick-block-symbol,
.alfa-modal-overlay.hf-torn .vault-trade-symbol,
.alfa-modal-overlay.hf-torn .quick-trade-symbol {
color: var(--hf-accent-text);
}
.hf-stocks-root .alfa-table th,
.hf-stocks-root .tx-table th,
.alfa-modal-overlay.hf-torn .alfa-table th,
.alfa-modal-overlay.hf-torn .tx-table th {
background: var(--hf-panel-header-bg);
border-bottom: 1px solid var(--hf-panel-border);
color: var(--hf-text-muted);
}
.hf-stocks-root .alfa-table td,
.hf-stocks-root .tx-table td,
.alfa-modal-overlay.hf-torn .alfa-table td,
.alfa-modal-overlay.hf-torn .tx-table td {
border-bottom: 1px solid var(--hf-panel-border);
color: var(--hf-text);
}
.hf-stocks-root .tx-table tbody tr:hover,
.alfa-modal-overlay.hf-torn .tx-table tbody tr:hover {
background: var(--hf-stock-card-hover);
}
.hf-stocks-root .alfa-hero:hover {
border-color: var(--hf-accent);
}
.hf-stocks-root .alfa-overlay {
z-index: 10050;
}
.hf-stocks-root .alfa-modal {
font-family: var(--hf-font);
}
.alfa-modal-overlay.hf-torn {
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.62);
backdrop-filter: blur(2px);
color: var(--hf-text);
font-family: var(--hf-font);
}
.alfa-modal-overlay.hf-torn .alfa-modal {
display: flex;
flex-direction: column;
width: min(680px, 94vw);
max-height: min(86vh, 760px);
overflow: hidden;
background: var(--hf-panel-bg);
border: 1px solid var(--hf-panel-border);
border-radius: var(--hf-radius);
box-shadow: var(--hf-shadow);
color: var(--hf-text);
font-family: var(--hf-font);
}
.alfa-modal-overlay.hf-torn .alfa-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
background: var(--hf-panel-header-bg);
border-bottom: 1px solid var(--hf-panel-border);
padding: 12px 14px;
min-height: 48px;
box-sizing: border-box;
}
.alfa-modal-overlay.hf-torn .alfa-modal-header h3 {
margin: 0;
padding: 0;
color: var(--hf-accent-text);
font-size: 14px;
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1.25;
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
}
.alfa-modal-overlay.hf-torn .alfa-modal-close {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 28px;
height: 28px;
margin: 0;
padding: 0;
color: var(--hf-text-muted);
font-size: 22px;
line-height: 1;
}
.alfa-modal-overlay.hf-torn .alfa-modal-close:hover {
color: var(--hf-hover);
}
.alfa-modal-overlay.hf-torn .alfa-modal-body {
flex: 1 1 auto;
min-height: 0;
max-height: none; /* override legacy 80vh \u2014 was causing modal + body double scroll */
overflow-y: auto;
color: var(--hf-text);
padding: 14px;
}
/* Rebalance / Spread / Withdraw layout
Warnings/summary/actions stay content-sized; only .rebal-section grows + scrolls.
(Previous grid 1fr row was assigned to the first warning \u2014 huge empty notice box.) */
.alfa-modal-overlay.hf-torn .alfa-modal-body:has(.rebal-container) {
display: flex;
flex-direction: column;
overflow: hidden;
}
.alfa-modal-overlay.hf-torn .rebal-container {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
gap: 10px;
overflow: hidden;
}
.alfa-modal-overlay.hf-torn .rebal-container > .rebal-summary,
.alfa-modal-overlay.hf-torn .rebal-container > .rebal-warning,
.alfa-modal-overlay.hf-torn .rebal-container > .rebal-actions,
.alfa-modal-overlay.hf-torn .rebal-container > [id$="-progress"],
.alfa-modal-overlay.hf-torn .rebal-container > [id$="-cash-received"] {
flex: 0 0 auto;
}
.alfa-modal-overlay.hf-torn .rebal-warning {
padding: 8px 10px;
line-height: 1.35;
}
.alfa-modal-overlay.hf-torn .rebal-section {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 160px;
overflow: hidden;
}
.alfa-modal-overlay.hf-torn .rebal-section-header,
.alfa-modal-overlay.hf-torn .rebal-subtotal {
flex: 0 0 auto;
}
.alfa-modal-overlay.hf-torn .rebal-table {
flex: 1 1 auto;
min-height: 0;
max-height: none;
overflow-x: hidden;
overflow-y: auto;
}
/* Portfolio rebalance: sell | buy side by side */
.alfa-modal-overlay.hf-torn .rebal-container:has(> .rebal-section ~ .rebal-section) {
display: grid;
grid-template-columns: 1fr 1fr;
grid-auto-rows: auto;
align-content: stretch;
gap: 10px;
}
.alfa-modal-overlay.hf-torn .rebal-container:has(> .rebal-section ~ .rebal-section) > .rebal-summary,
.alfa-modal-overlay.hf-torn .rebal-container:has(> .rebal-section ~ .rebal-section) > .rebal-warning,
.alfa-modal-overlay.hf-torn .rebal-container:has(> .rebal-section ~ .rebal-section) > .rebal-actions,
.alfa-modal-overlay.hf-torn .rebal-container:has(> .rebal-section ~ .rebal-section) > [id$="-progress"],
.alfa-modal-overlay.hf-torn .rebal-container:has(> .rebal-section ~ .rebal-section) > [id$="-cash-received"] {
grid-column: 1 / -1;
}
.alfa-modal-overlay.hf-torn .rebal-container:has(> .rebal-section ~ .rebal-section) > .rebal-section {
min-height: min(36vh, 260px);
max-height: min(42vh, 320px);
height: auto;
flex: unset;
}
.alfa-modal-overlay.hf-torn .rebal-row {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
overflow: hidden;
}
.alfa-modal-overlay.hf-torn .rebal-sym {
flex: 0 0 36px;
min-width: 36px;
}
.alfa-modal-overlay.hf-torn .rebal-signal {
flex: 0 0 44px;
min-width: 44px;
}
.alfa-modal-overlay.hf-torn .rebal-shares {
flex: 1 1 auto;
min-width: 0;
text-align: right;
}
.alfa-modal-overlay.hf-torn .rebal-value {
flex: 0 0 64px;
min-width: 64px;
text-align: right;
}
.alfa-modal-overlay.hf-torn .rebal-reason {
flex: 0 0 16px;
width: 16px;
min-width: 16px;
color: var(--hf-text-muted);
font-size: 12px;
line-height: 1;
text-align: center;
}
.alfa-modal-overlay.hf-torn .rebal-reason:empty {
display: none;
}
.alfa-modal-overlay.hf-torn .rebal-reason-tip {
display: inline-block;
cursor: help;
color: var(--hf-accent-text);
opacity: 0.85;
}
.alfa-modal-overlay.hf-torn .rebal-reason-tip:hover {
opacity: 1;
color: var(--hf-hover);
}
/* Mobile / PDA: modal body scrolls so Execute/Close stay reachable */
@media (max-width: 560px) {
.alfa-modal-overlay.hf-torn {
align-items: flex-start;
padding: 8px 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.alfa-modal-overlay.hf-torn .alfa-modal {
width: min(680px, 96vw);
max-height: min(92dvh, 92vh);
margin: auto;
}
.alfa-modal-overlay.hf-torn .alfa-modal-body:has(.rebal-container) {
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.alfa-modal-overlay.hf-torn .rebal-container {
overflow: visible;
flex: 0 0 auto;
min-height: auto;
height: auto;
}
.alfa-modal-overlay.hf-torn .rebal-container:has(> .rebal-section ~ .rebal-section) {
display: flex;
flex-direction: column;
grid-template-columns: unset;
}
.alfa-modal-overlay.hf-torn .rebal-section,
.alfa-modal-overlay.hf-torn .rebal-container:has(> .rebal-section ~ .rebal-section) > .rebal-section {
flex: 0 0 auto;
min-height: 0;
max-height: none;
height: auto;
overflow: visible;
}
.alfa-modal-overlay.hf-torn .rebal-table {
flex: 0 0 auto;
max-height: min(40vw, 200px);
overflow-y: auto;
}
.alfa-modal-overlay.hf-torn .rebal-actions {
position: sticky;
bottom: 0;
z-index: 2;
margin-top: 4px;
padding-top: 10px;
padding-bottom: 2px;
background: var(--hf-panel-bg);
flex-wrap: wrap;
}
.alfa-modal-overlay.hf-torn .rebal-actions .alfa-main-btn {
flex: 1 1 auto;
min-width: 120px;
}
}
.alfa-modal-overlay.hf-torn .settings-tabs,
.alfa-modal-overlay.hf-torn .swing-tabs,
.alfa-modal-overlay.hf-torn .tx-tabs {
border-bottom: 1px solid var(--hf-panel-border);
gap: 6px;
padding-bottom: 8px;
}
.alfa-modal-overlay.hf-torn .settings-section,
.alfa-modal-overlay.hf-torn .vault-config-section,
.alfa-modal-overlay.hf-torn .tx-summary-card,
.alfa-modal-overlay.hf-torn .quick-block-container {
color: var(--hf-text);
}
.alfa-modal-overlay.hf-torn .add-position-form {
color: var(--hf-text);
gap: 8px;
}
.alfa-modal-overlay.hf-torn .add-position-form .form-group {
gap: 3px;
}
.alfa-modal-overlay.hf-torn .add-position-form .form-summary {
padding: 10px 12px;
}
.alfa-modal-overlay.hf-torn .add-position-form #pos-notes.journal-notes-textarea {
min-height: 44px;
}
.alfa-modal-overlay.hf-torn .pos-source-modes {
margin: 0;
}
.alfa-modal-overlay.hf-torn .pos-source-options {
display: flex;
flex-wrap: wrap;
gap: 6px 12px;
margin-top: 4px;
}
.alfa-modal-overlay.hf-torn .pos-source-option,
.alfa-modal-overlay.hf-torn .swing-sell-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--hf-text);
cursor: pointer;
user-select: none;
}
.alfa-modal-overlay.hf-torn .swing-sell-toggle {
font-weight: 600;
}
/* Add Swing Position: compact custom stock dropdown (~half native height) */
.alfa-modal-overlay.hf-torn .pos-symbol-wrap {
position: relative;
}
.alfa-modal-overlay.hf-torn .pos-symbol-native {
position: absolute !important;
width: 1px !important;
height: 1px !important;
opacity: 0 !important;
pointer-events: none !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
}
.alfa-modal-overlay.hf-torn .pos-symbol-trigger {
width: 100%;
text-align: left;
cursor: pointer;
}
.alfa-modal-overlay.hf-torn .pos-symbol-menu {
position: absolute;
z-index: 40;
left: 0;
right: 0;
top: calc(100% + 4px);
max-height: 200px;
overflow-x: hidden;
overflow-y: auto;
background: var(--hf-stock-card-bg-strong);
border: 1px solid var(--hf-panel-border);
border-radius: 5px;
box-shadow: var(--hf-shadow);
padding: 4px;
}
.alfa-modal-overlay.hf-torn .pos-symbol-option {
display: block;
width: 100%;
appearance: none;
border: none;
background: transparent;
color: var(--hf-text);
font: inherit;
font-size: 12px;
text-align: left;
padding: 6px 8px;
border-radius: 4px;
cursor: pointer;
}
.alfa-modal-overlay.hf-torn .pos-symbol-option:hover,
.alfa-modal-overlay.hf-torn .pos-symbol-option.is-selected {
background: var(--hf-accent-subtle);
color: var(--hf-accent-text);
}
#smart-stocks-panel.hf-panel--grow .hf-panel-body {
padding-bottom: 16px;
}
.hf-stocks-root .alfa-vault-breakdown {
max-height: 400px;
}
.hf-stocks-root #vault-main-content {
overflow: visible;
}
`;
document.head.appendChild(style);
}
// src/stocks/main.js
var openVaultSettings = null;
var panel = null;
function buildMountRoot() {
const root = document.createElement("div");
root.id = "smart-stocks-root";
root.className = "hf-stocks-root";
return root;
}
async function init() {
HF.theme.injectStyles();
injectStocksStyles();
panel = ui.createPanel({
id: "smart-stocks-panel",
badge: "Smart Stock Vault",
mount: "stocksBar",
pageBody: "grow",
collapsedLegacyKeys: ["alfa_vault_collapsed"],
onSettings: () => openVaultSettings?.(),
tabs: [
{
id: "vault",
label: "Vault",
render: async () => buildMountRoot()
}
]
});
panel.ensureMounted();
bootstrapStocksApp({
mountRoot: "#smart-stocks-root",
registerSettings: (open) => {
openVaultSettings = open;
}
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();