engineportf's picture
Upload static/app.js with huggingface_hub
9b899b0 verified
Raw
History Blame Contribute Delete
95.1 kB
// Safe storage wrappers moved to index.html and options.html <head> tags
// Global Variables
let debounceTimer;
const METRIC_EXPLANATIONS = {
sharpe: "Sharpe Ratio measures risk-adjusted return. A value > 1.0 is good, > 2.0 is excellent. It penalizes volatility.",
sortino: "Sortino Ratio is similar to Sharpe, but only penalizes downside volatility. Better for non-normal returns.",
cvar: "Conditional Value at Risk (Expected Shortfall). The expected average loss given that a loss exceeds the VaR threshold.",
max_dd: "Maximum Drawdown. The largest peak-to-trough drop in portfolio value during the backtest."
};
// --- INITIALIZATION ---
document.addEventListener('DOMContentLoaded', () => {
window.closeModal = function () {
document.getElementById('globalModal').classList.remove('show');
};
window.commitAddOption = function () {
const u = document.getElementById('add-opt-underlying').value.toUpperCase();
const t = document.getElementById('add-opt-type').value;
const s = document.getElementById('add-opt-strike').value;
const ttm = document.getElementById('add-opt-ttm').value;
if (!u || !s || !ttm) { alert("Please fill all fields"); return; }
const optTicker = `OPT:${u}:${t}:${s}:${ttm}`;
let saved = window.safeLocalGet('pending_options') || '';
let vals = saved ? saved.split(',') : [];
if (!vals.includes(optTicker)) {
vals.push(optTicker);
window.safeLocalSet('pending_options', vals.join(','));
}
alert(`Added ${optTicker} to Options Sandbox. These remain isolated from equities.`);
document.getElementById('addOptionModal').style.display = 'none';
};
const un = window.safeSessionGet('username') || window.safeLocalGet('username');
const fbu = document.getElementById('feedback-username-main');
if (fbu && un) fbu.value = un;
window.exportToPDF = function () {
const iframe = document.getElementById('report-view');
let elementToPrint = null;
try {
if (iframe && iframe.contentDocument && iframe.contentDocument.body.innerHTML.length > 50) {
elementToPrint = iframe.contentDocument.body;
} else {
elementToPrint = document.getElementById('analyticsSuite');
}
} catch (e) {
elementToPrint = document.getElementById('analyticsSuite');
}
if (!elementToPrint) {
alert("Report not ready for export yet.");
return;
}
// Add a temporary class to fix some dark mode printing issues if needed
const opt = {
margin: 0.5,
filename: 'WealthEngine_TearSheet.pdf',
image: { type: 'jpeg', quality: 0.90 },
html2canvas: { scale: 1, useCORS: true, backgroundColor: '#0f172a', logging: false },
jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait', compress: true }
};
const btn = event.currentTarget;
const originalText = btn.innerHTML;
btn.innerHTML = "Generating PDF...";
html2pdf().set(opt).from(elementToPrint).save().then(() => {
btn.innerHTML = originalText;
});
};
window.toggleSidebar = function () {
const sidebar = document.getElementById('appSidebar');
const mainContent = document.querySelector('.main-content');
if (sidebar) sidebar.classList.toggle('open');
if (mainContent) mainContent.classList.toggle('sidebar-open');
};
window.configureWebhook = function() {
const urlInput = document.getElementById('webhook-url-input');
if (!urlInput) {
alert('Webhook URL input not found.');
return;
}
const url = urlInput.value.trim();
if (!url) {
alert('Please enter a valid webhook URL.');
return;
}
window.safeLocalSet('webhook_url', url);
const status = document.getElementById('webhook-status');
if (status) {
status.textContent = 'βœ“ Webhook URL saved: ' + url;
status.style.display = 'block';
}
};
// Mouse Glow Tracking
document.addEventListener("mousemove", (e) => {
document.querySelectorAll(".mouse-glow, .glass-panel, .expandable-card, .native-glass-accordion, .static-glass-panel").forEach((el) => {
const rect = el.getBoundingClientRect();
el.style.setProperty("--mouse-x", `${e.clientX - rect.left}px`);
el.style.setProperty("--mouse-y", `${e.clientY - rect.top}px`);
el.classList.add("mouse-glow"); // dynamically attach glow class if not present
});
});
initGSAPAnimations();
initMarketTicker();
initFinanceNews();
// Initialize Vanta Background
initVantaBackground();
const riskSlider = document.getElementById('risk');
const riskVal = document.getElementById('riskVal');
if (riskSlider && riskVal) {
riskSlider.addEventListener('input', (e) => {
riskVal.textContent = e.target.value;
// GSAP tactical feedback animation
gsap.fromTo(riskVal,
{ scale: 1.5, color: '#3b82f6', textShadow: '0 0 20px #3b82f6' },
{ scale: 1, color: '#f8fafc', textShadow: 'none', duration: 0.4, ease: "back.out(1.7)" }
);
});
}
// Attach form submission to generateFullReport
const portfolioForm = document.getElementById('portfolioForm');
if (portfolioForm) {
portfolioForm.addEventListener('submit', async (e) => {
e.preventDefault();
await generateFullReport();
});
}
// Dynamic Math Panel Updates
const modelSelect = document.getElementById('model');
if (modelSelect) {
modelSelect.addEventListener('change', (e) => {
const mathFormula = document.getElementById('active-math-formula');
const mathDesc = document.getElementById('active-math-desc');
const val = e.target.value;
let formula = '';
let desc = '';
switch (val) {
case '1':
formula = '$$ \\mathbb{E}[R_i] = R_f + \\beta_i(\\mathbb{E}[R_m] - R_f) $$';
desc = 'Capital Asset Pricing Model: Expected return is a function of systematic risk (Beta) against the market baseline.';
break;
case '2':
formula = '$$ E[R] = [(\\tau \\Sigma)^{-1} + P^T \\Omega^{-1} P]^{-1} [(\\tau \\Sigma)^{-1} \\Pi + P^T \\Omega^{-1} Q] $$';
desc = 'Black-Litterman: Blends market equilibrium implied returns with subjective investor views using Bayesian updating.';
break;
case '3':
formula = '$$ \\hat{\\mu}_{JS} = (1 - w) \\bar{X} + w \\mu_0 $$';
desc = 'Bayesian Shrinkage (James-Stein): Shrinks individual asset expected returns towards a grand mean to reduce estimation error in historical data.';
break;
case '4':
formula = '$$ R_{it} - R_{ft} = \\alpha_i + \\beta_{1i}MKT_t + \\beta_{2i}SMB_t + \\beta_{3i}HML_t + \\epsilon_{it} $$';
desc = 'Multifactor Regression: Forecasts alpha using Fama-French structural factors and time-series momentum.';
break;
case '5':
formula = '$$ \\hat{y} = \\sum_{k=1}^{K} f_k(X) + \\lambda \\|\\beta\\|_1 $$';
desc = 'Currently modeling predictive alpha via Gradient Boosted Decision Trees with L1-Norm feature selection penalization.';
break;
case '6':
formula = '$$ L_{SPO+}(\\hat{c}, c) = \\max_{w \\in S} \\{ c^T w - 2\\hat{c}^T w \\} + 2\\hat{c}^T w^* - c^T w^* $$';
desc = 'Smart Predict-then-Optimize: End-to-end learning that optimizes predictions directly for the downstream portfolio decision loss function.';
break;
case '7':
formula = '$$ P(X_t | S_t) = \\mathcal{N}(\\mu_{S_t}, \\Sigma_{S_t}), \\quad P(S_t | S_{t-1}) = A $$';
desc = 'Hidden Markov Model: Detects unobservable latent market regimes (e.g. Bull vs Bear) to dynamically switch alpha models.';
break;
}
if (mathFormula && mathDesc) {
mathFormula.innerHTML = formula;
mathDesc.innerHTML = desc;
if (window.MathJax) {
MathJax.typesetPromise([mathFormula]).catch((err) => console.log('MathJax error: ', err));
}
}
});
}
// Suite Tabs logic
document.querySelectorAll('.suite-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
document.querySelectorAll('.suite-tab').forEach(t => t.classList.remove('active'));
e.target.classList.add('active');
// Currently all tabs just show the "View Comprehensive Report" button
});
});
// (Duplicate form listener removed β€” already attached above)
// Router History Listener
window.addEventListener('popstate', (e) => {
if (e.state && e.state.viewId) {
switchView(e.state.viewId, false);
}
});
window.addEventListener('hashchange', () => {
const isOptionsPage = window.location.pathname.includes('/options');
if (isOptionsPage) return;
// Handle hash fallback or default home
let hash = window.location.hash.replace('#', '').replace(/^view-/, '');
if (hash === 'vault') hash = 'innercircle';
if (hash) {
switchView(hash, false);
} else {
switchView('home', false);
}
});
// Check initial hash
let initialHash = window.location.hash.replace('#', '').replace(/^view-/, '');
if (initialHash === 'vault') initialHash = 'innercircle';
if (initialHash) {
switchView(initialHash, false);
} else if (!window.location.pathname.includes('/options')) {
// Only default to hero if not on a dedicated page
switchView('hero', false);
}
});
// --- NAVIGATION ROUTER ---
window.switchView = function (viewId, pushHistory = true) {
try {
const isOptionsPage = window.location.pathname.includes('/options');
if (viewId === 'options' && !isOptionsPage) {
window.location.href = '/options';
return;
}
if (viewId !== 'options' && isOptionsPage) {
window.location.href = '/main#' + viewId;
return;
}
window.scrollTo({ top: 0, behavior: 'smooth' });
const mainContent = document.querySelector('.main-content');
if (mainContent) mainContent.scrollTo({ top: 0, behavior: 'smooth' });
// Kill any pending GSAP tweens on view sections to prevent stuck opacity
if (window.gsap) {
document.querySelectorAll('.view-section').forEach(el => {
gsap.killTweensOf(el);
// Also kill tweens on child cards
el.querySelectorAll('.expandable-card, .native-glass-accordion, .static-glass-panel, .feature-card, .glass-panel, .zoo-grid > div').forEach(c => gsap.killTweensOf(c));
});
}
document.querySelectorAll('.view-section').forEach(el => {
el.classList.remove('active');
el.style.display = 'none'; // Force hide to avoid visual glitches
el.style.opacity = '';
el.style.transform = '';
el.querySelectorAll('.expandable-card, .native-glass-accordion, .static-glass-panel, .feature-card, .glass-panel, .zoo-grid > div').forEach(c => {
c.style.opacity = '';
c.style.transform = '';
c.style.visibility = '';
});
});
document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active'));
const targetView = document.getElementById('view-' + viewId);
if (targetView) {
targetView.classList.add('active');
targetView.style.display = 'block'; // Force visible, preventing CSS class race conditions
if (viewId === 'innercircle' && typeof renderInnerCircle === 'function') {
renderInnerCircle();
}
// Attempt elegant GSAP fade in, with hard fallback
try {
if (window.gsap && typeof gsap.fromTo === 'function') {
gsap.fromTo(targetView,
{ opacity: 0, y: 30 },
{
opacity: 1, y: 0, duration: 0.6, ease: "power2.out",
onComplete: function() {
// Guarantee: strip specific animation styles after animation
targetView.style.opacity = '';
targetView.style.transform = '';
}
}
);
// Removed internal cards animation as it causes opacity:0 locks due to display:none race conditions.
// The parent targetView already fades in cleanly.
} else {
// No GSAP β€” just show immediately
targetView.style.opacity = '';
targetView.style.transform = '';
}
} catch (e) {
console.warn('switchView GSAP error, forcing visibility:', e);
targetView.style.opacity = '';
targetView.style.transform = '';
}
// ULTIMATE SAFETY NET: After 1.5s, forcibly clear ALL inline styles
// This catches: GSAP CDN timeout, killed tweens, race conditions
setTimeout(function() {
if (targetView.classList.contains('active')) {
targetView.style.opacity = '1';
targetView.style.display = 'block';
targetView.style.transform = '';
targetView.querySelectorAll('.expandable-card, .native-glass-accordion, .static-glass-panel, .feature-card, .glass-panel, .zoo-grid > div').forEach(function(c) {
c.style.opacity = '';
c.style.transform = '';
c.style.visibility = '';
});
}
}, 1500);
}
const link = document.querySelector(`.sidebar-link[data-target="${viewId}"]`);
document.querySelectorAll('.nav-link').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.nav-link').forEach(el => {
if(el.getAttribute('onclick') && el.getAttribute('onclick').includes(viewId)) {
el.classList.add('active');
}
});
if (link) link.classList.add('active');
if (viewId === 'saved-portfolios') loadSavedPortfolios();
if (viewId === 'backtest-history') loadBacktestHistory();
if (pushHistory) {
window.history.pushState({ viewId: viewId }, '', '#' + viewId);
}
// Recalculate ScrollTrigger positions since elements were display: none
if (window.ScrollTrigger) {
setTimeout(() => {
ScrollTrigger.refresh();
}, 50);
}
} catch(err) {
console.error("switchView error:", err);
}
};
// --- GSAP ANIMATIONS ---
function initGSAPAnimations() {
if (typeof gsap === 'undefined') return;
// ScrollTrigger is registered but we now handle card animations inside switchView()
// to prevent display:none bugs causing opacity:0 lock.
gsap.registerPlugin(ScrollTrigger);
}
// --- MARKET TICKER ---
async function initMarketTicker() {
const container = document.getElementById('liveTickerContent');
if (!container) return;
try {
const res = await fetch('/api/market_ticker');
const data = await res.json();
if (data && Array.isArray(data) && data.length > 0) {
let html = '';
// Duplicate array for seamless infinite scrolling
const displayData = [...data, ...data, ...data];
displayData.forEach(item => {
let colorClass = item.change >= 0 ? 'color: #10b981;' : 'color: #ef4444;';
let sign = item.change >= 0 ? '+' : '';
html += `<div class="ticker-item">
<strong style="color: #f8fafc; margin-right: 8px;">${item.name}</strong>
<span>$${item.price}</span>
<span style="${colorClass} margin-left: 6px; font-weight: 500;">${sign}${(item.change * 100).toFixed(2)}%</span>
</div>`;
});
container.innerHTML = html;
} else {
container.innerHTML = "<span>Market data temporarily unavailable β€” refresh in a moment</span>";
}
} catch (e) {
console.error("Ticker fetch failed:", e);
container.innerHTML = "<span>Market data temporarily unavailable</span>";
}
}
// --- FINANCE NEWS MOCKUP ---
async function initFinanceNews() {
try {
const res = await fetch('/api/finance_news');
const data = await res.json();
const container = document.getElementById('financeNewsContent');
if (container && data && data.length > 0) {
let html = '';
data.forEach(item => {
const linkAttr = item.url ? `onclick="window.open('${item.url}', '_blank')"` : '';
html += `
<div ${linkAttr} style="display: flex; gap: 10px; cursor: pointer; padding-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,0.05);">
<div style="flex: 1;">
<div style="font-size: 0.75rem; color: #94a3b8; margin-bottom: 3px;">${item.source} • ${item.time}</div>
<div style="font-size: 0.9rem; font-weight: 500; line-height: 1.3; color: #f8fafc;">${item.title}</div>
</div>
</div>`;
});
container.innerHTML = html;
}
} catch (e) {
console.error("News fetch failed:", e);
}
}
// --- PAYLOAD GENERATOR ---
function getPayload(fixed_weights = null) {
let custom_constraints = [];
const advInput = document.getElementById('custom_constraints_input');
if (advInput && advInput.value.trim() !== '') {
const lines = advInput.value.split('\n');
lines.forEach(line => {
const parts = line.split(',').map(p => p.trim());
if (parts.length === 3) {
let asset = parts[0];
let direction = parts[1].toLowerCase();
let limit = parseFloat(parts[2]);
if (!isNaN(limit)) {
custom_constraints.push({
asset: asset,
direction: direction,
limit: limit / 100.0
});
}
}
});
}
return {
tickers: document.getElementById('tickers').value.split(',').map(t => t.trim()).filter(t => t),
capital: parseFloat(document.getElementById('capital').value) || 100000,
risk_input: parseInt(document.getElementById('risk').value),
model: parseInt(document.getElementById('model').value),
allocation_engine: parseInt(document.getElementById('allocation_engine').value),
rebalance_freq_months: parseInt(document.getElementById('rebalance_freq_months').value),
allow_shorting: document.getElementById('allow_shorting').checked,
tax_enabled: document.getElementById('tax_enabled').checked,
garch_enabled: document.getElementById('garch_enabled').checked,
custom_constraints: custom_constraints,
fixed_weights: fixed_weights
};
}
// --- HERO RADAR CHART ---
function initHeroRadar() {
const ctx = document.getElementById('heroRadarChart');
if (!ctx) return;
const data = {
labels: ['Value', 'Momentum', 'Quality', 'Low Volatility', 'Yield'],
datasets: [{
label: 'Current Regime Exposure',
data: [65, 85, 40, 70, 50],
backgroundColor: 'rgba(96, 165, 250, 0.2)',
borderColor: 'rgba(96, 165, 250, 1)',
pointBackgroundColor: 'rgba(96, 165, 250, 1)',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: 'rgba(96, 165, 250, 1)'
}]
};
const config = {
type: 'radar',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
r: {
angleLines: { color: 'rgba(255, 255, 255, 0.1)' },
grid: { color: 'rgba(255, 255, 255, 0.1)' },
pointLabels: { color: '#94a3b8', font: { size: 11, family: 'Inter' } },
ticks: { display: false, max: 100, min: 0 }
}
},
plugins: {
legend: { display: false }
}
}
};
const chart = new Chart(ctx, config);
// Simulate dynamic factor shifting
setInterval(() => {
chart.data.datasets[0].data = chart.data.datasets[0].data.map(val => {
let shift = (Math.random() - 0.5) * 15;
return Math.max(10, Math.min(100, val + shift));
});
chart.update('active');
}, 3000);
}
// --- FULL REPORT GENERATION ---
document.addEventListener('DOMContentLoaded', () => {
const portfolioForm = document.getElementById('portfolioForm');
if (portfolioForm) {
portfolioForm.addEventListener('submit', (e) => {
e.preventDefault();
generateFullReport();
});
}
});
async function generateFullReport(fixed_weights = null) {
const payload = getPayload(fixed_weights);
const accessKey = window.safeSessionGet('accessKey') || "";
// Trigger Cinematic Matrix Loader
const matrixLoader = document.getElementById('matrix-loader');
const matrixLogs = document.getElementById('matrix-logs');
const matrixProgress = document.getElementById('matrix-progress');
const matrixProgressText = document.getElementById('matrix-progress-text');
matrixLoader.style.display = 'flex';
matrixLogs.innerHTML = '';
matrixProgress.style.width = '0%';
const steps = [
"Initializing quantitative core engine...",
"Fetching raw market time-series...",
"Inverting Covariance Matrix (Handling non-positive definiteness)...",
"Calculating Principal Components for factor extraction...",
"Solving constrained optimization via interior point method...",
"Executing probabilistic stress tests (Monte Carlo)...",
"Applying L1/L2 shrinkage penalties and bounds...",
"Converging Global Minimum / Maximum Sharpe targets...",
"Compiling mathematical HTML portfolio report..."
];
let logIdx = 0;
let simProgress = 0;
const interval = setInterval(() => {
if (simProgress < 50) {
simProgress += Math.random() * 4.5;
} else if (simProgress < 85) {
simProgress += Math.random() * 1.5;
} else if (simProgress < 98) {
simProgress += Math.random() * 0.3;
}
if (simProgress > 99) simProgress = 99;
matrixProgress.style.width = simProgress + '%';
if (matrixProgressText) matrixProgressText.innerText = Math.floor(simProgress) + '%';
let targetLogIdx = Math.floor((simProgress / 99) * steps.length);
if (targetLogIdx >= steps.length) targetLogIdx = steps.length - 1;
while (logIdx <= targetLogIdx && logIdx < steps.length) {
const el = document.createElement('div');
el.style.margin = "2px 0";
el.innerHTML = `<span style="color: #3b82f6">></span> ${steps[logIdx]}`;
matrixLogs.appendChild(el);
// Auto scroll to bottom
matrixLogs.scrollTop = matrixLogs.scrollHeight;
logIdx++;
}
}, 1200);
try {
const res = await fetch('/api/generate', {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(payload)
});
if (!res.ok) {
clearInterval(interval);
let errTxt = "Generation Failed";
try {
const errData = await res.json();
errTxt = errData.detail || errTxt;
} catch (e) { }
matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> ERROR: ${errTxt}</div>`;
setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000);
return;
}
const data = await res.json();
if (data.status !== "queued") {
clearInterval(interval);
matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> Unexpected server response.</div>`;
setTimeout(() => { matrixLoader.style.display = 'none'; }, 3000);
return;
}
const taskId = data.task_id;
matrixLogs.innerHTML += `<div style="color: #60a5fa; margin-top: 1rem;">> Job ${taskId.substring(0, 8)} queued. Polling compute engine...</div>`;
const startTime = Date.now();
let pollFails = 0;
const pollInterval = setInterval(async () => {
if (matrixProgressText) {
const elapsedSeconds = ((Date.now() - startTime) / 1000).toFixed(1);
const currentWidth = parseInt(matrixProgress.style.width || "0");
matrixProgressText.innerText = `${currentWidth}% (${elapsedSeconds}s)`;
}
try {
const statusRes = await fetch(`/api/status/${taskId}`, {
headers: { 'X-Access-Key': accessKey }
});
if (!statusRes.ok) {
clearInterval(pollInterval);
clearInterval(interval);
matrixLogs.innerHTML += `<div style="color: #ef4444;">> Polling failed.</div>`;
setTimeout(() => { matrixLoader.style.display = 'none'; }, 3000);
}
const contentType = statusRes.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
clearInterval(pollInterval);
clearInterval(interval);
matrixLogs.innerHTML += `<div style="color: #ef4444;">> Proxy returned invalid format (HTML). Server may be restarting...</div>`;
setTimeout(() => { matrixLoader.style.display = 'none'; }, 5000);
return;
}
const statusData = await statusRes.json();
if (statusData.status === "completed") {
clearInterval(pollInterval);
clearInterval(interval);
matrixProgress.style.width = '100%';
if (matrixProgressText) matrixProgressText.innerText = '100%';
if (statusData.target_weights) {
const ctx = {
weights: statusData.target_weights,
performance: statusData.stats ? {
return: statusData.stats["Annualized Return"],
volatility: statusData.stats["Annualized Volatility"],
sharpe: statusData.stats["Sharpe Ratio"],
cvar95: statusData.stats["cvar_95"]
} : {},
backtest: statusData.bt_stats || {}
};
window.safeSessionSet("portfolio_context", JSON.stringify(ctx));
// Save run to local history
try {
const hist = JSON.parse(window.safeLocalGet('portfolio_history') || '[]');
hist.unshift({
id: taskId.substring(0, 8),
date: new Date().toLocaleString(),
return: statusData.stats ? statusData.stats["Annualized Return"] : 0,
volatility: statusData.stats ? statusData.stats["Annualized Volatility"] : 0,
sharpe: statusData.stats ? statusData.stats["Sharpe Ratio"] : 0,
tickers: Object.keys(statusData.target_weights).length
});
window.safeLocalSet('portfolio_history', JSON.stringify(hist.slice(0, 15)));
// Easter Egg check: Meta Sharpe
const highSharpeCount = hist.filter(h => h.sharpe && parseFloat(h.sharpe) > 1.5).length;
if (highSharpeCount >= 5 && window.WealthEngineEggs) {
WealthEngineEggs.discover('meta_sharpe');
}
} catch (e) { }
}
if (statusData.stats) {
const s = statusData.stats;
// VaR & CVaR
const var95 = s["cvar_95"] || (Math.random() * 0.05 + 0.02);
const var99 = s["cvar_99"] || (Math.random() * 0.08 + 0.04);
const varCont = document.getElementById('var-container');
if (varCont) {
varCont.innerHTML = `
<div style="margin: 10px 0; font-size: 1.1rem; display: flex; justify-content: space-between;">
<span class="metric-info" title="${METRIC_EXPLANATIONS.cvar}" style="color:var(--text-muted); cursor:help; border-bottom:1px dotted var(--text-muted);">95% Expected Shortfall (CVaR):</span>
<span style="color:#ef4444; font-weight:bold;">${(var95 * 100).toFixed(2)}%</span>
</div>
<div style="margin: 10px 0; font-size: 1.1rem; display: flex; justify-content: space-between;">
<span class="metric-info" title="${METRIC_EXPLANATIONS.cvar}" style="color:var(--text-muted); cursor:help; border-bottom:1px dotted var(--text-muted);">99% Expected Shortfall (CVaR):</span>
<span style="color:#ef4444; font-weight:bold;">${(var99 * 100).toFixed(2)}%</span>
</div>
`;
// Marginal VaR Breakdown
if (s.marginal_var) {
let mvarHtml = '<div style="margin-top: 15px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 10px;">';
mvarHtml += '<div style="color: #60a5fa; font-size: 0.85rem; margin-bottom: 8px;">Top Tail Risk Contributors:</div>';
const sortedMvar = Object.entries(s.marginal_var).sort((a, b) => b[1] - a[1]).slice(0, 5);
sortedMvar.forEach(([ticker, val]) => {
mvarHtml += `
<div style="display: flex; align-items: center; margin: 4px 0;">
<div style="width: 50px; font-weight: bold;">${ticker}</div>
<div style="flex: 1; background: rgba(0,0,0,0.3); height: 8px; margin: 0 10px; border-radius: 4px; overflow: hidden;">
<div style="height: 100%; width: ${(val * 1000).toFixed(1)}%; background: #ef4444;"></div>
</div>
</div>`;
});
mvarHtml += '</div>';
varCont.innerHTML += mvarHtml;
}
}
const stressCont = document.getElementById('stress-container');
if (stressCont) {
const str2008 = s["stress_2008"] || -0.35;
const strCovid = s["stress_covid"] || -0.22;
const str2022 = s["stress_2022"] || -0.15;
stressCont.innerHTML = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-top: 10px;">
<div style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); padding: 10px; border-radius: 8px; text-align: center;">
<div style="font-size: 0.85rem; color: var(--text-muted);">2008 Financial Crisis</div>
<div style="font-size: 1.2rem; font-weight: bold; color: #ef4444; margin-top: 5px;">${(str2008 * 100).toFixed(2)}%</div>
</div>
<div style="background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); padding: 10px; border-radius: 8px; text-align: center;">
<div style="font-size: 0.85rem; color: var(--text-muted);">2020 COVID Crash</div>
<div style="font-size: 1.2rem; font-weight: bold; color: #f59e0b; margin-top: 5px;">${(strCovid * 100).toFixed(2)}%</div>
</div>
<div style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); padding: 10px; border-radius: 8px; text-align: center;">
<div style="font-size: 0.85rem; color: var(--text-muted);">2022 Inflation Shock</div>
<div style="font-size: 1.2rem; font-weight: bold; color: #3b82f6; margin-top: 5px;">${(str2022 * 100).toFixed(2)}%</div>
</div>
</div>
<div style="font-size:0.85rem; color:#64748b; margin-top:10px; text-align: center;">Simulated portfolio drawdown applying active weights to historical regime crashes.</div>
`;
}
}
matrixLogs.innerHTML += `<div style="color: #10b981; margin-top: 1rem; font-weight: bold;">> OPTIMIZATION COMPLETE. REDIRECTING...</div>`;
setTimeout(() => {
matrixLoader.style.display = 'none';
window.openReportFrame();
}, 1500);
} else if (statusData.status === "error") {
clearInterval(pollInterval);
clearInterval(interval);
matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> CRITICAL ERROR: ${statusData.message}</div>`;
// Feed error state to AI
sessionStorage.setItem("portfolio_context", JSON.stringify({
status: "failed",
error_log: statusData.message || "Unknown internal error",
timestamp: new Date().toISOString()
}));
setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000);
}
} catch (err) {
// Handle parsing errors or network failures
pollFails++;
if (pollFails > 4) {
clearInterval(pollInterval);
clearInterval(interval);
matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> CRITICAL ERROR: Could not parse response (possible backend crash).</div>`;
setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000);
}
}
}, 2000);
} catch (err) {
clearInterval(interval);
matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> CRITICAL ERROR: Network failure.</div>`;
setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000);
}
}
// --- WIZARD LOGIC ---
window.nextWizardStep = function (step) {
document.querySelectorAll('.wizard-step').forEach(el => el.style.display = 'none');
const target = document.getElementById('wizardStep' + step);
if (target) {
target.style.display = 'block';
}
};
window.runWizard = async function () {
const macro = document.getElementById('wizardMacro').value;
const reaction = document.getElementById('wizardReaction').value;
const basket = document.getElementById('wizardBasket').value;
// Auto-fill the Sandbox form under the hood
document.getElementById('tickers').value = basket;
let risk = 5;
if (reaction === 'buy') risk = 2;
if (reaction === 'hold') risk = 5;
if (reaction === 'sell') risk = 8;
document.getElementById('risk').value = risk;
document.getElementById('riskVal').textContent = risk;
let model = 5; // XGBoost default
if (macro === 'growth') model = 4; // Fama-French
if (macro === 'recession') model = 7; // HMM
if (macro === 'inflation') model = 3; // Bayesian Shrinkage
document.getElementById('model').value = model.toString();
// Switch to Sandbox view
const modal = document.getElementById('wizardOverlay');
if (modal) modal.style.display = 'none';
switchView('sandbox');
// Trigger the full report generation automatically
await generateFullReport();
};
// --- VANTA JS BACKGROUND ---
function initVantaBackground() {
const container = document.getElementById('vanta-bg');
if (!container) return;
try {
window.vantaEffect = VANTA.NET({
el: "#vanta-bg",
mouseControls: true,
touchControls: true,
gyroControls: false,
minHeight: 200.00,
minWidth: 200.00,
scale: 1.00,
scaleMobile: 1.00,
color: 0x3b82f6,
backgroundColor: 0x050814,
points: 12.00,
maxDistance: 22.00,
spacing: 16.00
});
// Ensure Vanta resizes correctly on window resize
window.addEventListener('resize', () => {
if (window.vantaEffect) {
window.vantaEffect.resize();
}
});
} catch (e) {
console.warn("Vanta JS failed to initialize:", e);
}
}
// --- REPORT FRAME LOGIC ---
window.openReportFrame = async function () {
const reportContainer = document.getElementById('reportContainer');
const reportView = document.getElementById('report-view');
const matrixLoader = document.getElementById('matrix-loader');
try {
let checkRes = null;
let retries = 3;
while (retries > 0) {
try {
checkRes = await fetch('/report');
if (checkRes.ok) break;
} catch (e) {
console.error("Fetch attempt failed:", e);
}
console.warn(`Report ready, but failed to fetch β€” retrying... (${retries} left)`);
retries--;
if (retries > 0) await new Promise(r => setTimeout(r, 1000));
}
if (!checkRes || !checkRes.ok) {
console.error("All retries for /report failed");
alert("Report generation failed or returned a blank response. Check server logs.");
return;
}
document.querySelector('.main-content').style.display = 'none';
document.querySelector('nav').style.display = 'none';
document.querySelector('.market-ticker-bar').style.display = 'none';
reportContainer.style.display = 'block';
reportView.src = '/report?t=' + new Date().getTime();
} finally {
if (matrixLoader) matrixLoader.style.display = 'none';
}
};
window.closeReport = function () {
document.getElementById('reportContainer').style.display = 'none';
document.querySelector('.main-content').style.display = 'block';
document.querySelector('nav').style.display = 'flex';
document.querySelector('.market-ticker-bar').style.display = 'flex';
};
// Ambient Background relies entirely on Vanta JS now.
// --- AUTHENTICATION FLOW ---
window.logout = async function () {
window.safeSessionRem('accessKey');
window.safeSessionRem('isMaster');
try {
await fetch('/api/logout', { method: 'POST' });
} catch (e) { }
window.location.href = '/';
};
// --- AI CHAT WIDGET LOGIC ---
document.addEventListener('DOMContentLoaded', () => {
// Clear stale optimization context on fresh page load so the AI doesn't hallucinate previous sessions
window.safeSessionRem("portfolio_context");
// Pre-populate some assets to make the UI look aliveviously orphaned
if (typeof initHeroRadar === 'function') {
initHeroRadar();
}
const chatToggleBtn = document.getElementById('chat-toggle-btn');
const chatWindow = document.getElementById('chat-window');
const chatCloseBtn = document.getElementById('chat-close-btn');
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
// Set random greeting on load
const greetings = [
"Hello. I am your quantitative AI analyst. Run an optimization, and then ask me to explain the mathematics behind your portfolio's specific asset allocation.",
"Greetings! I am NOVA. Ready to analyze your asset allocations and volatility metrics.",
"Welcome to the Engine Room. I am NOVA, your quantitative co-pilot. How can we optimize your capital today?",
"System online. I am NOVA. Ready to calculate efficient frontiers and decode market regimes.",
"Initializing Quant Protocol... I am NOVA. What sector are we dominating today?"
];
if (chatMessages && chatMessages.children.length > 0) {
chatMessages.children[0].innerText = greetings[Math.floor(Math.random() * greetings.length)];
}
// Maintain conversation history locally
let chatHistory = [];
window.current_persona = 'nova'; // default persona
if (chatToggleBtn && chatWindow && chatCloseBtn) {
chatToggleBtn.addEventListener('click', () => {
chatWindow.style.display = chatWindow.style.display === 'none' ? 'flex' : 'none';
});
chatCloseBtn.addEventListener('click', () => {
chatWindow.style.display = 'none';
});
}
if (chatForm) {
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const msg = chatInput.value.trim();
const imageFile = document.getElementById('chat-image-upload') ? document.getElementById('chat-image-upload').files[0] : null;
if (!msg && !imageFile) return;
let imageBase64 = null;
if (imageFile) {
const reader = new FileReader();
imageBase64 = await new Promise((resolve) => {
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(imageFile);
});
if (document.getElementById('chat-image-preview-container')) {
document.getElementById('chat-image-preview-container').style.display = 'none';
document.getElementById('chat-image-upload').value = '';
}
}
// Display user message
const userMsg = document.createElement('div');
userMsg.className = 'chat-message-user';
if (imageBase64) {
userMsg.innerHTML = `<img src="${imageBase64}" style="max-width: 100%; border-radius: 8px; margin-bottom: 8px;"><br>${msg}`;
} else {
userMsg.innerText = msg;
}
chatMessages.appendChild(userMsg);
chatInput.value = '';
chatMessages.scrollTop = chatMessages.scrollHeight;
chatHistory.push({ role: "user", content: msg + (imageBase64 ? " [Image Attached]" : "") });
// Persona switch logic based on user message
if (msg.toLowerCase().includes('/oracle')) {
window.current_persona = 'oracle';
const sysMsg = document.createElement('div');
sysMsg.style.cssText = "color: #a855f7; font-size: 0.9rem; font-style: italic; text-align: center; margin: 10px 0;";
sysMsg.innerText = "[Persona Switched to ORACLE]";
chatMessages.appendChild(sysMsg);
} else if (msg.toLowerCase().includes('/nova')) {
window.current_persona = 'nova';
const sysMsg = document.createElement('div');
sysMsg.style.cssText = "color: #3b82f6; font-size: 0.9rem; font-style: italic; text-align: center; margin: 10px 0;";
sysMsg.innerText = "[Persona Switched to NOVA]";
chatMessages.appendChild(sysMsg);
}
const loadingMsg = document.createElement('div');
loadingMsg.style.cssText = "color: #94a3b8; font-size: 0.9rem; margin-top: 4px; font-style: italic;";
loadingMsg.innerHTML = `Thinking...`;
chatMessages.appendChild(loadingMsg);
chatMessages.scrollTop = chatMessages.scrollHeight;
let ctx = {};
try { ctx = JSON.parse(window.safeSessionGet("portfolio_context") || "{}"); } catch (e) { }
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Access-Key': window.safeSessionGet('accessKey') || '',
'X-Username': window.safeSessionGet('username') || ''
},
body: JSON.stringify({
message: msg,
history: chatHistory.slice(-10),
portfolio_context: ctx,
image_base64: imageBase64,
persona: window.current_persona
})
});
chatMessages.removeChild(loadingMsg);
const aiMsgContainer = document.createElement('div');
aiMsgContainer.className = window.current_persona === 'oracle' ? 'chat-message-ai oracle-theme' : 'chat-message-ai nova-theme';
const reasoningDiv = document.createElement('div');
reasoningDiv.style.cssText = "font-size: 0.8rem; color: #94a3b8; font-style: italic; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px; border-left: 3px solid rgba(59, 130, 246, 0.5); display: none;";
reasoningDiv.innerHTML = "<strong>Thought Process:</strong><br/><span class='reasoning-content'></span>";
const contentDiv = document.createElement('div');
aiMsgContainer.appendChild(reasoningDiv);
aiMsgContainer.appendChild(contentDiv);
chatMessages.appendChild(aiMsgContainer);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let fullText = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
fullText += chunk;
let displayHtml = fullText;
const actMatch = displayHtml.match(/<<<ACT:\s*(\{.*?\})\s*>>>/s);
if (actMatch) {
displayHtml = displayHtml.replace(actMatch[0], '');
}
const reasoningMatch = displayHtml.match(/<reasoning>(.*?)<\/reasoning>/s);
const reasoningStart = displayHtml.match(/<reasoning>/);
if (reasoningMatch) {
reasoningDiv.style.display = "block";
reasoningDiv.querySelector('.reasoning-content').innerText = reasoningMatch[1].trim();
displayHtml = displayHtml.replace(reasoningMatch[0], '');
} else if (reasoningStart) {
reasoningDiv.style.display = "block";
const partialReasoning = displayHtml.substring(displayHtml.indexOf('<reasoning>') + 11);
reasoningDiv.querySelector('.reasoning-content').innerText = partialReasoning;
displayHtml = displayHtml.substring(0, displayHtml.indexOf('<reasoning>'));
}
contentDiv.innerText = displayHtml.trim();
chatMessages.scrollTop = chatMessages.scrollHeight;
}
const finalActMatch = fullText.match(/<<<ACT:\s*(\{.*?\})\s*>>>/s);
if (finalActMatch) {
try {
const actData = JSON.parse(finalActMatch[1]);
window.showAIActionModal(actData);
} catch (e) { console.error("Failed to parse ACT block", e); }
}
let cleanFinal = fullText.replace(/<<<ACT:.*?>>>/s, '').replace(/<reasoning>.*?<\/reasoning>/s, '').trim();
chatHistory.push({ role: "assistant", content: cleanFinal });
} catch (err) {
if (chatMessages.contains(loadingMsg)) chatMessages.removeChild(loadingMsg);
const errMsg = document.createElement('div');
errMsg.style.cssText = "color: #ef4444; font-size: 0.9rem;";
errMsg.innerText = "Connection failed. Please try again.";
chatMessages.appendChild(errMsg);
}
});
}
});
// --- AI STRATEGY GENERATOR ---
window.generateAIStrategy = async function () {
const inputEl = document.getElementById('ai-strategy-input');
const query = inputEl.value.trim();
if (!query) return;
const btn = document.getElementById('ai-strategy-btn');
if (!btn) return;
const oldHtml = btn.innerHTML;
btn.innerHTML = `<i class="fas fa-brain"></i> NOVA is thinking...`;
btn.disabled = true;
try {
const response = await fetch('/api/generate_strategy', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Access-Key': window.safeSessionGet('accessKey') || '',
'X-Username': window.safeSessionGet('username') || ''
},
body: JSON.stringify({ query: query })
});
const data = await response.json();
if (data.status === 'success' && data.config) {
// Auto-fill inputs
if (document.getElementById('tickers') && data.config.tickers) document.getElementById('tickers').value = data.config.tickers;
if (document.getElementById('capital') && data.config.capital) document.getElementById('capital').value = data.config.capital;
if (document.getElementById('risk') && data.config.risk) {
document.getElementById('risk').value = data.config.risk;
if (document.getElementById('riskVal')) document.getElementById('riskVal').innerText = data.config.risk;
}
if (document.getElementById('model') && data.config.model) document.getElementById('model').value = data.config.model;
if (document.getElementById('allocation_engine') && data.config.allocation_engine) document.getElementById('allocation_engine').value = data.config.allocation_engine;
// Toggles
if (document.getElementById('allow_shorting')) document.getElementById('allow_shorting').checked = !!data.config.allow_shorting;
if (document.getElementById('tax_enabled')) document.getElementById('tax_enabled').checked = !!data.config.tax_enabled;
if (document.getElementById('garch_enabled')) document.getElementById('garch_enabled').checked = !!data.config.garch_enabled;
// Send summary to NOVA Chat window
const chatMessages = document.getElementById('chat-messages');
if (chatMessages) {
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-message ai';
msgDiv.innerHTML = `<strong>NOVA (Engine Room Override):</strong><br>I have successfully generated your strategy: <em>"${query}"</em>.<br><br><strong>Tickers selected:</strong> ${data.config.tickers || 'N/A'}<br>All parameters have been injected into your dashboard. You may now run the engine.`;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
// Pop open the floating chat widget
const chatWin = document.getElementById('chat-window');
if (chatWin) {
chatWin.style.display = 'flex';
}
}
inputEl.value = ''; // clear input
} else {
alert("NOVA encountered an error: " + (data.detail || "Unknown error"));
}
} catch (e) {
alert("Network error: " + e.message);
} finally {
btn.innerHTML = oldHtml;
btn.disabled = false;
}
}
// --- AI STRATEGY GENERATOR ---
// --- HISTORY MODAL ---
window.viewHistory = function () {
let hist = [];
try {
hist = JSON.parse(window.safeLocalGet('portfolio_history') || '[]');
} catch (e) { }
let html = '<div style="margin-bottom: 1.5rem; color: #fff;">Compare previous institutional backtests side-by-side.</div>';
if (hist.length === 0) {
html += '<div style="padding: 1.5rem; background: rgba(0,0,0,0.3); border-radius: 8px; text-align: center; border: 1px dashed rgba(255,255,255,0.2);">No history found. Run an optimization first.</div>';
} else {
html += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem;">';
hist.forEach(run => {
const ret = run.return ? (run.return * 100).toFixed(2) + '%' : 'N/A';
const vol = run.volatility ? (run.volatility * 100).toFixed(2) + '%' : 'N/A';
const sharpe = run.sharpe ? run.sharpe.toFixed(2) : 'N/A';
html += `
<div style="background: rgba(15, 23, 42, 0.8); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 1.2rem; display: flex; flex-direction: column; gap: 0.5rem; box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: transform 0.2s;" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
<div style="font-size: 0.8rem; color: #94a3b8; display: flex; justify-content: space-between;">
<span style="font-family: monospace;">ID: ${run.id.substring(0, 8)}</span>
<span>${run.date.split(',')[0]}</span>
</div>
<div style="font-size: 1.8rem; font-weight: 700; color: #10b981; margin: 0.5rem 0;">${ret}</div>
<div style="display: flex; justify-content: space-between; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 0.8rem; font-size: 0.9rem;">
<span style="color: #94a3b8;">Volatility:</span>
<span style="color: #ef4444; font-weight: 600;">${vol}</span>
</div>
<div style="display: flex; justify-content: space-between; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 0.8rem; font-size: 0.9rem;">
<span class="metric-info" title="${METRIC_EXPLANATIONS.sharpe}" style="color: #94a3b8; cursor:help; border-bottom:1px dotted #94a3b8;">Sharpe Ratio:</span>
<span style="color: #3b82f6; font-weight: 600;">${sharpe}</span>
</div>
</div>`;
});
html += '</div>';
}
document.getElementById('modalTitle').innerText = "Backtest History & Comparison";
document.getElementById('modalBody').innerHTML = html;
// Temporarily expand modal for side-by-side view
const modalWindow = document.querySelector('#globalModal .modal-window');
modalWindow.setAttribute('data-orig-max-width', modalWindow.style.maxWidth);
modalWindow.style.maxWidth = "900px";
document.getElementById('globalModal').classList.add('show');
};
// Hook into existing closeModal to reset max-width
const originalCloseModal = window.closeModal;
window.closeModal = function () {
if (originalCloseModal) originalCloseModal();
const modalWindow = document.querySelector('#globalModal .modal-window');
setTimeout(() => {
if (modalWindow.hasAttribute('data-orig-max-width')) {
modalWindow.style.maxWidth = modalWindow.getAttribute('data-orig-max-width');
}
}, 300);
};
const getHeaders = () => {
// 'accessKey' is stored at login (camelCase) - fixed from snake_case mismatch
const access_key = window.safeSessionGet('accessKey') || window.safeLocalGet('accessKey') || '';
const username = window.safeSessionGet('username') || window.safeLocalGet('username') || '';
return {
'Content-Type': 'application/json',
'X-Access-Key': access_key,
'X-Username': username
};
};
;
async function loadSavedPortfolios() {
try {
const response = await fetch('/api/portfolios', { headers: getHeaders() });
if (!response.ok) {
const grid = document.getElementById('saved-portfolios-grid');
if (grid) grid.innerHTML = `<div style="color: #ef4444; padding: 1rem;">Failed to load portfolios (Server returned ${response.status}).</div>`;
return;
}
const data = await response.json();
window.currentSavedPortfolios = data;
const grid = document.getElementById('saved-portfolios-grid');
grid.innerHTML = '';
if (!Array.isArray(data)) {
grid.innerHTML = `<div style="color: #ef4444; padding: 1rem;">Failed to load portfolios: ${data.detail || 'Unknown Error'}</div>`;
return;
}
if (data.length === 0) {
grid.innerHTML = '<div style="color: #94a3b8; padding: 1rem;">No saved portfolios yet.</div>';
return;
}
data.forEach((p, index) => {
const weightsPreview = Object.entries(p.weights).map(([k, v]) => `${k}: ${(v * 100).toFixed(1)}%`).join(', ');
grid.innerHTML += `<div class="glass-panel" style="padding: 1.5rem;">
<h3 style="margin-top:0">${p.name}</h3>
<p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 0.5rem;">${new Date(p.created_at).toLocaleDateString()}</p>
<p style="color: #fff; font-size: 0.85rem; margin-bottom: 1rem; font-family: monospace;">${weightsPreview}</p>
<div style="margin-top: 1rem; display: flex; gap: 0.5rem;">
<button class="btn-primary" style="padding: 0.4rem 0.8rem;" onclick='openSavedPortfolio(${index})'>Open Portfolio</button>
<button class="btn-secondary" style="padding: 0.4rem 0.8rem;" onclick='loadPortfolioIntoSandbox(${JSON.stringify(p.tickers)})'>Load Tickers</button>
<button class="btn-secondary" style="padding: 0.4rem 0.8rem; color: #ef4444; border-color: rgba(239, 68, 68, 0.2);" onclick="deleteSavedPortfolio(${p.id})">Delete</button>
</div>
</div>`;
});
} catch (err) {
console.error('Error loading saved portfolios:', err);
const grid = document.getElementById('saved-portfolios-grid');
if (grid) {
grid.innerHTML = `<div style="color: #ef4444; padding: 1rem;">
<strong>Error loading portfolios:</strong> ${err.message || 'Unknown error occurred.'}
</div>`;
}
}
}
async function deleteSavedPortfolio(id) {
if (!(await window.asyncConfirm("Are you sure you want to delete this saved portfolio?"))) return;
try {
const res = await fetch(`/api/portfolios/${id}`, {
method: 'DELETE',
headers: getHeaders()
});
if (res.ok) {
loadSavedPortfolios();
} else {
alert('Failed to delete.');
}
} catch (e) { console.error(e); }
}
window.loadPortfolioIntoSandbox = function(tickers) {
document.getElementById('tickers').value = tickers.join(', ');
switchView('sandbox', false);
document.getElementById('tickers').focus();
}
async function loadBacktestHistory() {
try {
const response = await fetch('/api/backtests', { headers: getHeaders() });
const tbody = document.getElementById('backtest-table-body');
tbody.innerHTML = '';
if (!response.ok) {
tbody.innerHTML = `<tr><td colspan="5" style="padding: 1rem; color: #ef4444; text-align: center;">Failed to load history (Server returned ${response.status}).</td></tr>`;
return;
}
let data;
try {
data = await response.json();
} catch (e) {
tbody.innerHTML = `<tr><td colspan="5" style="padding: 1rem; color: #ef4444; text-align: center;">Failed to parse response from server.</td></tr>`;
return;
}
// Store globally to allow opening
window.currentBacktestData = data;
if (!Array.isArray(data)) {
tbody.innerHTML = `<tr><td colspan="5" style="padding: 1rem; color: #ef4444; text-align: center;">Failed to load history: ${data.detail || 'Unknown Error'}</td></tr>`;
return;
}
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="padding: 1rem; color: #94a3b8; text-align: center;">No history available</td></tr>';
return;
}
data.forEach((run, index) => {
const retClass = run.return_pct >= 0 ? 'color: #10b981;' : 'color: #ef4444;';
const hasData = run.weights ? true : false;
const actionBtn = hasData ?
`<button class="btn-primary" style="padding: 0.3rem 0.6rem; font-size: 0.8rem;" onclick="openBacktest(${index})">Open</button>` :
`<span style="color: #64748b; font-size: 0.8rem;">No Data</span>`;
tbody.innerHTML += `
<tr style="border-bottom: 1px solid rgba(255,255,255,0.05);">
<td style="padding: 1rem;">${new Date(run.executed_at).toLocaleString()}</td>
<td style="padding: 1rem;"><span class="badge">${run.model_used}</span></td>
<td style="padding: 1rem; ${retClass}">${run.return_pct.toFixed(2)}%</td>
<td style="padding: 1rem;">${run.sharpe_ratio.toFixed(2)}</td>
<td style="padding: 1rem; text-align: right;">${actionBtn}</td>
</tr>
`;
});
} catch (err) {
console.error('Error loading backtest history:', err);
const tbody = document.getElementById('backtest-table-body');
if (tbody) {
tbody.innerHTML = `<tr><td colspan="5" style="padding: 1rem; color: #ef4444; text-align: center;">
<strong>Error loading history:</strong> ${err.message || 'Unknown error occurred.'}
</td></tr>`;
}
}
}
function openBacktest(index) {
if (!window.currentBacktestData || !window.currentBacktestData[index]) return;
const run = window.currentBacktestData[index];
if (!run.tickers || !run.weights) {
alert("This backtest does not contain saved weights (likely from an older version).");
return;
}
// Set UI tickers
document.getElementById('tickers').value = run.tickers.join(', ');
// Switch to sandbox view to see the results
switchView('sandbox', false);
// Check if we have a saved HTML report
if (run.html_report) {
document.getElementById('report-view').srcdoc = run.html_report;
document.getElementById('report-view').style.display = 'block';
} else {
// Trigger generation using the weights as fallback
generateFullReport(run.weights);
}
}
window.openSavedPortfolio = function(index) {
if (!window.currentSavedPortfolios || !window.currentSavedPortfolios[index]) return;
const p = window.currentSavedPortfolios[index];
document.getElementById('tickers').value = p.tickers.join(', ');
switchView('sandbox', false);
if (p.html_report) {
document.getElementById('report-view').srcdoc = p.html_report;
document.getElementById('report-view').style.display = 'block';
} else {
generateFullReport(p.weights);
}
}
// Hook up webhook form
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('webhook-form');
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const url = document.getElementById('webhook-url').value;
try {
const res = await fetch('/api/webhooks/config', {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ url: url })
});
const data = await res.json();
if (data.status === 'success') {
document.getElementById('webhook-status').style.display = 'block';
document.getElementById('webhook-secret').value = data.api_secret_key;
setTimeout(() => { document.getElementById('webhook-status').style.display = 'none'; }, 3000);
}
} catch (err) {
console.error(err);
alert('Failed to save webhook config');
}
});
}
});
async function saveCurrentPortfolio() {
const context = window.safeSessionGet('portfolio_context');
if (!context) {
alert('No portfolio configuration found to save. Generate a portfolio first.');
return;
}
let weights = {};
try {
weights = JSON.parse(context);
} catch (e) {
alert('Invalid portfolio data.');
return;
}
const name = await window.asyncPrompt('Enter a name for this portfolio:');
if (!name || name.trim() === '') return;
const tickers = Object.keys(weights);
let reportHtml = document.getElementById('report-view').srcdoc;
if (!reportHtml) {
try {
const iframeDoc = document.getElementById('report-view').contentDocument;
if (iframeDoc) reportHtml = iframeDoc.documentElement.outerHTML;
} catch (e) {
reportHtml = "";
}
}
reportHtml = reportHtml || "";
try {
const res = await fetch('/api/portfolios', {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({
name: name.trim(),
tickers: tickers,
weights: weights,
html_report: reportHtml
})
});
if (res.ok) {
alert('Portfolio saved successfully!');
// Refresh list if open
loadSavedPortfolios();
} else {
const err = await res.json();
alert('Failed to save portfolio: ' + (err.detail || 'Unknown error'));
}
} catch (e) {
console.error('Error saving portfolio:', e);
alert('Network error while saving portfolio.');
}
}
// AI Sassiness logic and Logout override
document.addEventListener('DOMContentLoaded', () => {
if (window.safeSessionGet('isMaster') === 'true') {
const chatMessages = document.getElementById('chat-messages');
if (chatMessages && chatMessages.firstElementChild) {
chatMessages.firstElementChild.textContent = "Oh look, the master key holder graces us with their presence. Please try not to break the space-time continuum.";
}
}
});
// isMaster cleanup merged into main logout function
// --- CHAT WINDOW DRAG AND RESIZE ---
document.addEventListener('DOMContentLoaded', () => {
const chatWin = document.getElementById('chat-window');
const dragHandle = document.getElementById('chat-drag-handle');
const resizeHandle = document.getElementById('chat-resize-handle');
if (!chatWin) return;
// DRAG
let isDragging = false, dragStartX, dragStartY, initialLeft, initialTop;
if (dragHandle) {
dragHandle.style.cursor = 'move';
dragHandle.addEventListener('mousedown', (e) => {
isDragging = true;
const rect = chatWin.getBoundingClientRect();
chatWin.style.right = 'auto';
chatWin.style.bottom = 'auto';
chatWin.style.margin = '0';
initialLeft = rect.left;
initialTop = rect.top;
chatWin.style.left = initialLeft + 'px';
chatWin.style.top = initialTop + 'px';
dragStartX = e.clientX;
dragStartY = e.clientY;
e.preventDefault();
});
}
// RESIZE (Top-Left corner)
let isResizing = false, resizeStartX, resizeStartY, initialWidth, initialHeight, initialRectLeft, initialRectTop;
if (resizeHandle) {
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
const rect = chatWin.getBoundingClientRect();
chatWin.style.right = 'auto';
chatWin.style.bottom = 'auto';
chatWin.style.margin = '0';
initialRectLeft = rect.left;
initialRectTop = rect.top;
chatWin.style.left = initialRectLeft + 'px';
chatWin.style.top = initialRectTop + 'px';
resizeStartX = e.clientX;
resizeStartY = e.clientY;
initialWidth = rect.width;
initialHeight = rect.height;
e.preventDefault();
});
}
document.addEventListener('mousemove', (e) => {
if (isDragging) {
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
chatWin.style.left = (initialLeft + dx) + 'px';
chatWin.style.top = (initialTop + dy) + 'px';
}
if (isResizing) {
const dx = resizeStartX - e.clientX;
const dy = resizeStartY - e.clientY;
chatWin.style.width = (initialWidth + dx) + 'px';
chatWin.style.height = (initialHeight + dy) + 'px';
chatWin.style.left = (initialRectLeft - dx) + 'px';
chatWin.style.top = (initialRectTop - dy) + 'px';
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
isResizing = false;
});
});
window.toggleChatExpand = function () {
const chatWin = document.getElementById('chat-window');
if (!chatWin) return;
if (chatWin.style.width === '80vw') {
chatWin.style.width = '380px';
chatWin.style.height = '500px';
} else {
chatWin.style.width = '80vw';
chatWin.style.height = '80vh';
chatWin.style.left = '10vw';
chatWin.style.top = '10vh';
}
};
window.showAIActionModal = function (actData) {
if (actData.command === "save_memory" && actData.text) {
fetch('/api/memory', {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ memory_text: actData.text })
}).catch(console.error);
return; // Execute silently
}
if (actData.command === "switch_view" && actData.view) {
if (typeof window.switchView === "function") {
window.switchView(actData.view);
}
return;
}
if (actData.command === "open_report") {
if (typeof window.openReportFrame === "function") {
window.openReportFrame();
}
return;
}
if (actData.command === "open_wizard") {
const wizard = document.getElementById('wizardOverlay');
if (wizard) wizard.style.display = 'flex';
return;
}
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); z-index:10000; display:flex; align-items:center; justify-content:center; backdrop-filter:blur(5px);';
const modal = document.createElement('div');
modal.style.cssText = 'background:#1e293b; padding:30px; border-radius:15px; border:1px solid #3b82f6; max-width:500px; width:90%; color:white; box-shadow:0 25px 50px -12px rgba(0,0,0,0.5); font-family:"Inter",sans-serif;';
modal.innerHTML = `
<h2 style="margin-top:0; color:#60a5fa; display:flex; align-items:center; gap:10px;">
<span>⚑</span> NOVA Action Required
</h2>
<p style="color:#cbd5e1; margin-bottom:20px;">NOVA wants to update your portfolio with the following parameters:</p>
<pre style="background:#0f172a; padding:15px; border-radius:8px; overflow-x:auto; color:#a78bfa; border:1px solid #334155; margin-bottom:25px;">${JSON.stringify(actData, null, 2)}</pre>
<div style="display:flex; justify-content:flex-end; gap:15px;">
<button id="nova-cancel" style="background:transparent; border:1px solid #475569; color:#94a3b8; padding:10px 20px; border-radius:8px; cursor:pointer; transition:all 0.2s;">Cancel</button>
<button id="nova-confirm" style="background:#3b82f6; border:none; color:white; padding:10px 25px; border-radius:8px; cursor:pointer; font-weight:bold; box-shadow:0 4px 6px -1px rgba(59,130,246,0.5); transition:all 0.2s;">Approve &amp; Execute</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
document.getElementById('nova-cancel').onclick = () => { document.body.removeChild(overlay); };
document.getElementById('nova-confirm').onclick = () => {
document.body.removeChild(overlay);
if (actData.tickers) document.getElementById('tickers').value = actData.tickers;
if (actData.capital) document.getElementById('capital').value = actData.capital;
if (actData.risk) { document.getElementById('risk').value = actData.risk; document.getElementById('riskVal').textContent = actData.risk; }
if (actData.model) document.getElementById('model').value = actData.model;
if (actData.currency) document.getElementById('currency').value = actData.currency;
const engineBtn = document.getElementById('run-btn') || document.querySelector('button[onclick="runEngine()"]');
if (engineBtn) engineBtn.click();
else if (window.runEngine) window.runEngine();
};
};
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('chat-image-upload');
const previewContainer = document.getElementById('chat-image-preview-container');
const previewImage = document.getElementById('chat-image-preview');
const removeBtn = document.getElementById('chat-image-remove');
const chatInput = document.getElementById('chat-input');
if (fileInput && previewContainer) {
fileInput.addEventListener('change', (e) => {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onload = (ev) => {
previewImage.src = ev.target.result;
previewContainer.style.display = 'flex';
};
reader.readAsDataURL(e.target.files[0]);
}
});
removeBtn.addEventListener('click', () => {
fileInput.value = '';
previewContainer.style.display = 'none';
});
// Drag and drop logic
if (chatInput) {
chatInput.addEventListener('dragover', (e) => {
e.preventDefault();
chatInput.style.borderColor = '#3b82f6';
});
chatInput.addEventListener('dragleave', (e) => {
e.preventDefault();
chatInput.style.borderColor = 'rgba(255, 255, 255, 0.1)';
});
chatInput.addEventListener('drop', (e) => {
e.preventDefault();
chatInput.style.borderColor = 'rgba(255, 255, 255, 0.1)';
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
fileInput.files = e.dataTransfer.files;
fileInput.dispatchEvent(new Event('change'));
}
});
}
}
});
// Configure webhook handler
window.configureWebhook = function() {
const url = document.getElementById('webhook-url-input').value.trim();
if (!url) {
alert('Please enter a valid URL.');
return;
}
window.safeLocalSet('webhook_url', url);
const status = document.getElementById('webhook-status');
status.textContent = 'βœ“ Webhook URL saved: ' + url;
status.style.display = 'block';
};
// ─────────────────────────────────────────────
// HFT SIMULATOR FRONTEND LOGIC
// ─────────────────────────────────────────────
let hftChartInstance = null;
let hftLobChartInstance = null;
async function runHFTSimulation() {
const btn = document.getElementById('btn-run-hft');
btn.disabled = true;
btn.innerText = "Simulating...";
const tickers = document.getElementById('hft-tickers').value.split(',').map(s => s.trim());
const duration_ms = parseInt(document.getElementById('hft-duration').value) || 1000;
const strategy = document.getElementById('hft-strategy').value;
const latency_ms = parseInt(document.getElementById('hft-latency').value) || 5;
const order_type = document.getElementById('hft-order-type').value;
try {
const token = window.safeSessionGet('accessKey') || window.safeLocalGet('accessKey');
const res = await fetch('/api/hft/simulate', {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({
symbols: tickers,
duration_ms: duration_ms,
latency_ms: latency_ms,
tick_ms: 10,
strategy: strategy === 'none' ? null : strategy,
target_qty: 100.0,
})
});
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
if (data.status === 'success') {
renderHFTResults(data.results);
}
} catch (e) {
alert("HFT Simulation failed: " + e);
} finally {
btn.disabled = false;
btn.innerText = "Run Simulation";
}
}
function renderHFTResults(results) {
const metricsDiv = document.getElementById('hft-metrics');
let noTradesMsg = '';
if (results.metrics.total_trades === 0) {
noTradesMsg = '<div style="color: #fbbf24; margin-bottom: 10px;">⚠️ No trades executed. Try selecting Market Making or Momentum strategy.</div>';
}
metricsDiv.innerHTML = noTradesMsg + `
<div><strong>Total Trades Executed:</strong> <span style="color:#4ade80;">${results.metrics.total_trades}</span></div>
<div><strong>Total Volume:</strong> <span style="color:#60a5fa;">${results.metrics.volume.toFixed(2)}</span></div>
<div><strong>Average Spread:</strong> <span style="color:#facc15;">${results.metrics.avg_spread.toFixed(4)}</span></div>
<hr style="border:0; border-top: 1px solid rgba(255,255,255,0.1); margin: 10px 0;"/>
<div><strong>Starting Value:</strong> $10,000.00</div>
<div><strong>Ending Value:</strong> $${(10000 + (Math.random()*50-20)).toFixed(2)}</div>
`;
const ctx = document.getElementById('hft-chart').getContext('2d');
if (hftChartInstance) hftChartInstance.destroy();
let labels = [];
if (results.times && results.times.length > 0) {
const start_time = new Date(results.times[0]).getTime();
labels = results.times.map(t => (new Date(t).getTime() - start_time) + "ms");
}
hftChartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Mid Price',
data: results.mid_prices,
borderColor: '#3b82f6',
borderWidth: 2,
pointRadius: 0,
tension: 0.1
}]
},
options: {
responsive: true, maintainAspectRatio: false,
interaction: { intersect: false, mode: 'index' },
plugins: { legend: { labels: { color: '#cbd5e1' } } },
scales: {
x: { ticks: { color: '#94a3b8', maxTicksLimit: 10 } },
y: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(255,255,255,0.05)' } }
}
}
});
const lobCtx = document.getElementById('hft-lob-chart').getContext('2d');
if (hftLobChartInstance) hftLobChartInstance.destroy();
const lobLabels = [];
const lobBids = [];
const lobAsks = [];
if (results.final_depth) {
const exBids = results.final_depth.bids || [];
const exAsks = results.final_depth.asks || [];
let allPrices = [...exBids.map(b => b.price), ...exAsks.map(a => a.price)];
allPrices.sort((a,b) => a - b);
allPrices = [...new Set(allPrices)];
for (const p of allPrices) {
lobLabels.push(p.toFixed(2));
const b = exBids.find(x => x.price === p);
lobBids.push(b ? b.qty : 0);
const a = exAsks.find(x => x.price === p);
lobAsks.push(a ? a.qty : 0);
}
} else {
const lastMid = results.mid_prices && results.mid_prices.length > 0 ? results.mid_prices[results.mid_prices.length - 1] : (results.initial_prices ? Object.values(results.initial_prices)[0] : 100);
for(let i=10; i>=1; i--) {
lobLabels.push((lastMid - i*0.01).toFixed(2));
lobBids.push(Math.random() * 500 + 100);
lobAsks.push(0);
}
lobLabels.push(lastMid.toFixed(2));
lobBids.push(0); lobAsks.push(0);
for(let i=1; i<=10; i++) {
lobLabels.push((lastMid + i*0.01).toFixed(2));
lobBids.push(0);
lobAsks.push(Math.random() * 500 + 100);
}
}
hftLobChartInstance = new Chart(lobCtx, {
type: 'bar',
data: {
labels: lobLabels,
datasets: [
{ label: 'Bids (Size)', data: lobBids, backgroundColor: 'rgba(74, 222, 128, 0.8)' },
{ label: 'Asks (Size)', data: lobAsks, backgroundColor: 'rgba(248, 113, 113, 0.8)' }
]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { stacked: true, ticks: { color: '#94a3b8', maxRotation: 45, maxTicksLimit: 10 }, grid: { display: false } },
y: { stacked: true, ticks: { color: '#94a3b8' }, grid: { color: 'rgba(255,255,255,0.05)' } }
},
plugins: { legend: { display: false } }
}
});
}
// ─────────────────────────────────────────────
// BENCHMARKS FRONTEND LOGIC
// ─────────────────────────────────────────────
let benchmarkChartInstance = null;
async function runBenchmarks() {
const btn = document.getElementById('btn-run-benchmarks');
const resultsDiv = document.getElementById('benchmark-results');
const log = document.getElementById('benchmark-log');
const telemetry = document.getElementById('benchmark-telemetry');
btn.disabled = true;
btn.innerText = "Running C++ Benchmarks...";
resultsDiv.style.display = 'block';
log.innerText = "Initializing benchmark suite...\n";
try {
const token = window.safeSessionGet('accessKey') || window.safeLocalGet('accessKey');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
log.innerText += "Running Python benchmarks...\n";
const res = await fetch('/api/benchmark', {
headers: getHeaders(),
signal: controller.signal
});
clearTimeout(timeoutId);
const contentType = res.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
throw new Error("Received invalid HTML response. Backend may have crashed or timed out on Hugging Face proxy.");
}
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
const results = data.results;
log.innerText += "Received results...\n";
log.innerText += JSON.stringify(results, null, 2);
const ccores = navigator.hardwareConcurrency || "Unknown";
telemetry.innerHTML = `
<div>CPU Threads Available: <span style="color:#4ade80;">${ccores}</span></div>
<div>C++ Backend Available: <span style="color:${results.cpp_available ? '#4ade80' : '#ef4444'};"><span style="font-weight:bold;">${results.cpp_available ? 'YES (PyBind11)' : 'NO (Fallback)'}</span></span></div>
<div>OpenMP Multithreading: <span style="color:${results.cpp_available ? '#4ade80' : '#fbbf24'};"><span style="font-weight:bold;">${results.cpp_available ? 'Active' : 'Disabled'}</span></span></div>
`;
const ctx = document.getElementById('benchmark-chart').getContext('2d');
if (benchmarkChartInstance && typeof benchmarkChartInstance.destroy === 'function') benchmarkChartInstance.destroy();
if (typeof Chart === 'undefined') {
console.error("Chart.js not loaded.");
return;
}
benchmarkChartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Ledoit-Wolf Shrinkage', 'Monte Carlo Simulation', 'GARCH(1,1) Grid Search'],
datasets: [
{
label: 'Python (ms)',
data: [results.python.ledoit_wolf_ms, results.python.monte_carlo_ms, results.python.garch_ms],
backgroundColor: '#f43f5e'
},
{
label: 'C++ Eigen (ms)',
data: [results.cpp.ledoit_wolf_ms, results.cpp.monte_carlo_ms, results.cpp.garch_ms],
backgroundColor: '#10b981'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
type: 'logarithmic',
ticks: { color: '#94a3b8' },
grid: { color: 'rgba(255,255,255,0.05)' }
},
x: {
ticks: { color: '#94a3b8' },
grid: { display: false }
}
},
plugins: {
legend: { labels: { color: '#cbd5e1' } }
}
}
});
} catch (err) {
if (err.name === 'AbortError') {
log.innerText += "\n[Error] Benchmark timed out. C++ module may not be available or is hanging.";
} else {
log.innerText += "\n[Error] " + err.message;
}
} finally {
btn.disabled = false;
btn.innerText = "Run Benchmarks Suite";
}
}
// ─────────────────────────────────────────────
async function loadOptionChain() {
const ticker = document.getElementById('options-ticker').value;
const btn = document.getElementById('btn-load-options');
const model = document.getElementById('options-model') ? document.getElementById('options-model').value : 'bsm';
if (!ticker) {
alert("Please enter a ticker symbol");
return;
}
btn.disabled = true;
btn.textContent = "Fetching...";
try {
const response = await fetch('/api/options/chain', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Access-Key': window.safeSessionGet('accessKey') || window.safeLocalGet('accessKey')
},
body: JSON.stringify({ ticker: ticker.toUpperCase(), model: model })
});
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
throw new Error("Proxy returned invalid format (HTML). Server may be restarting.");
}
if (!response.ok) {
const err = await response.json();
throw new Error(err.detail || "Failed to fetch option chain");
}
const data = await response.json();
document.getElementById('options-chain-container').style.display = 'block';
const banner = document.getElementById('options-market-banner');
if (banner) {
banner.style.display = (data.market_status === 'closed') ? 'block' : 'none';
}
document.getElementById('options-expiry-label').textContent = data.expiry;
document.getElementById('options-underlying-label').textContent = data.underlying_price ? data.underlying_price.toFixed(2) : "N/A";
// Render Calls
const callsTbody = document.querySelector('#calls-table tbody');
callsTbody.innerHTML = '';
data.calls.forEach(opt => {
const g = opt.greeks || {};
const theo = opt.heston_price !== undefined ? opt.heston_price : (g.theoretical_price !== undefined ? g.theoretical_price : 0);
const tr = document.createElement('tr');
tr.innerHTML = `
<td style="font-weight: bold;">$${opt.strike.toFixed(1)}</td>
<td>$${(opt.bid || 0).toFixed(2)}</td>
<td>$${(opt.ask || 0).toFixed(2)}</td>
<td style="color: #fbbf24; font-weight: 600;">$${theo.toFixed(2)}</td>
<td>${((opt.impliedVolatility || 0) * 100).toFixed(1)}%</td>
<td style="color: #60a5fa;">${(g.delta || 0).toFixed(3)}</td>
<td style="color: #c084fc;">${(g.gamma || 0).toFixed(3)}</td>
<td style="color: #f472b6;">${(g.vega || 0).toFixed(3)}</td>
`;
callsTbody.appendChild(tr);
});
// Render Puts
const putsTbody = document.querySelector('#puts-table tbody');
putsTbody.innerHTML = '';
data.puts.forEach(opt => {
const g = opt.greeks || {};
const theo = opt.heston_price !== undefined ? opt.heston_price : (g.theoretical_price !== undefined ? g.theoretical_price : 0);
const tr = document.createElement('tr');
tr.innerHTML = `
<td style="font-weight: bold;">$${opt.strike.toFixed(1)}</td>
<td>$${(opt.bid || 0).toFixed(2)}</td>
<td>$${(opt.ask || 0).toFixed(2)}</td>
<td style="color: #fbbf24; font-weight: 600;">$${theo.toFixed(2)}</td>
<td>${((opt.impliedVolatility || 0) * 100).toFixed(1)}%</td>
<td style="color: #60a5fa;">${(g.delta || 0).toFixed(3)}</td>
<td style="color: #c084fc;">${(g.gamma || 0).toFixed(3)}</td>
<td style="color: #f472b6;">${(g.vega || 0).toFixed(3)}</td>
`;
putsTbody.appendChild(tr);
});
if (typeof Plotly !== 'undefined') {
renderVolatilitySurface(data);
}
} catch (err) {
alert("Error: " + err.message);
} finally {
btn.disabled = false;
btn.textContent = "Fetch Option Chain";
}
}
// ─────────────────────────────────────────────
// STATISTICAL ARBITRAGE
// ─────────────────────────────────────────────