`;
el.innerHTML = html;
}
// ---- Known tickers lookup table (offline-first, no API needed) ----
const TICKER_DB = {
// ETFs
'VTI' :{ name:'Vanguard Total Stock Market ETF', price:298.42, sector:'US Equity', change:0.64 },
'VXUS':{ name:'Vanguard Total International ETF', price:64.18, sector:'Intl Equity', change:0.31 },
'BND' :{ name:'Vanguard Total Bond Market ETF', price:73.85, sector:'Bonds', change:-0.08 },
'VNQ' :{ name:'Vanguard Real Estate ETF', price:88.12, sector:'Real Estate', change:0.22 },
'VOO' :{ name:'Vanguard S&P 500 ETF', price:524.85, sector:'US Equity', change:0.58 },
'SPY' :{ name:'SPDR S&P 500 ETF', price:524.10, sector:'US Equity', change:0.57 },
'QQQ' :{ name:'Invesco Nasdaq-100 ETF', price:462.30, sector:'Tech', change:0.82 },
'IVV' :{ name:'iShares Core S&P 500 ETF', price:525.20, sector:'US Equity', change:0.57 },
'SCHD':{ name:'Schwab US Dividend Equity ETF', price:107.14, sector:'US Equity', change:0.21 },
'EMB' :{ name:'iShares JP Morgan EM Bond ETF', price:93.75, sector:'Bonds', change:0.12 },
'GLD' :{ name:'SPDR Gold Shares ETF', price:429.50, sector:'Hard Assets', change:0.62 },
'SLV' :{ name:'iShares Silver Trust ETF', price:30.18, sector:'Hard Assets', change:0.44 },
'COPX':{ name:'Global X Copper Miners ETF', price:43.21, sector:'Hard Assets', change:0.31 },
'ITA' :{ name:'iShares US Aerospace & Defense ETF', price:138.20, sector:'Defense', change:0.44 },
'XLK' :{ name:'Technology Select Sector SPDR ETF', price:232.10, sector:'Tech', change:0.91 },
'XLE' :{ name:'Energy Select Sector SPDR ETF', price:91.40, sector:'Energy', change:-0.32 },
'XLV' :{ name:'Health Care Select Sector SPDR ETF', price:148.20, sector:'Healthcare', change:0.15 },
'XLF' :{ name:'Financial Select Sector SPDR ETF', price:51.30, sector:'US Equity', change:0.38 },
'SGOV':{ name:'iShares 0-3 Month Treasury Bond ETF', price:100.52, sector:'Bonds', change:0.01 },
'TLT' :{ name:'iShares 20+ Year Treasury Bond ETF', price:88.40, sector:'Bonds', change:-0.22 },
'VTIP':{ name:'Vanguard Short-Term Inflation ETF', price:47.80, sector:'Bonds', change:0.08 },
'IBIT':{ name:'iShares Bitcoin Trust ETF', price:58.42, sector:'Crypto', change:1.24 },
// Stocks
'NVDA':{ name:'Nvidia Corporation', price:225.61, sector:'Tech', change:0.94 },
'AAPL':{ name:'Apple Inc.', price:211.45, sector:'Tech', change:0.32 },
'MSFT':{ name:'Microsoft Corporation', price:447.20, sector:'Tech', change:0.48 },
'AMZN':{ name:'Amazon.com Inc.', price:214.80, sector:'Tech', change:0.67 },
'GOOGL':{ name:'Alphabet Inc. Class A', price:179.30, sector:'Tech', change:0.52 },
'META':{ name:'Meta Platforms Inc.', price:618.40, sector:'Tech', change:0.71 },
'TSLA':{ name:'Tesla Inc.', price:248.50, sector:'Tech', change:1.82 },
'BRK.B':{ name:'Berkshire Hathaway Inc. Class B', price:467.20, sector:'US Equity', change:0.18 },
'JPM' :{ name:'JPMorgan Chase & Co.', price:268.40, sector:'US Equity', change:0.44 },
'V' :{ name:'Visa Inc.', price:349.10, sector:'US Equity', change:0.29 },
'JNJ' :{ name:'Johnson & Johnson', price:148.20, sector:'Healthcare', change:-0.12 },
'PG' :{ name:'Procter & Gamble Co.', price:171.80, sector:'US Equity', change:0.22 },
'LMT' :{ name:'Lockheed Martin Corporation', price:516.50, sector:'Defense', change:-0.38 },
'RTX' :{ name:'RTX Corporation', price:138.20, sector:'Defense', change:0.41 },
'ANET':{ name:'Arista Networks Inc.', price:112.40, sector:'Tech', change:0.88 },
'VRT' :{ name:'Vertiv Holdings Co.', price:124.30, sector:'Tech', change:1.12 },
'MRVL':{ name:'Marvell Technology Inc.', price:87.50, sector:'Tech', change:0.94 },
'OKLO':{ name:'Oklo Inc.', price:21.43, sector:'Energy', change:2.14 },
'RKLB':{ name:'Rocket Lab USA Inc.', price:20.80, sector:'Tech', change:1.88 },
'SCHW':{ name:'Charles Schwab Corporation', price:82.10, sector:'US Equity', change:0.31 },
'WMT' :{ name:'Walmart Inc.', price:98.40, sector:'US Equity', change:0.18 },
'HD' :{ name:'The Home Depot Inc.', price:384.20, sector:'US Equity', change:0.24 },
};
// Auto-lookup when ticker is entered
async function lookupTicker(sym) {
sym = (sym || '').trim().toUpperCase();
if (!sym || sym.length < 1) return;
const statusEl = document.getElementById('hf_lookupStatus');
const nameEl = document.getElementById('hf_name');
const priceEl = document.getElementById('hf_price');
const sourceEl = document.getElementById('hf_priceSource');
const sectorEl = document.getElementById('hf_sector');
const changeEl = document.getElementById('hf_change');
// Helper to set status
const setStatus = (msg, color) => {
if (statusEl) { statusEl.textContent = msg; statusEl.style.color = color || 'var(--ink-faint)'; }
};
const setSource = (msg, color) => {
if (sourceEl) { sourceEl.textContent = msg; sourceEl.style.color = color || 'var(--green)'; }
};
// ── Fast path: offline DB cache ──
// Still check it first for speed, but ALWAYS fall through to live if user
// typed something we don't recognise
if (TICKER_DB[sym] && nameEl.value === '' && priceEl.value === '') {
const t = TICKER_DB[sym];
nameEl.value = t.name;
priceEl.value = t.price;
if (sectorEl) sectorEl.value = t.sector;
if (changeEl) changeEl.value = t.change || 0;
setSource('✓ CACHED', 'var(--green)');
setStatus('✓ ' + t.name + ' · $' + t.price + ' (cached — click LOOKUP for live price)', 'var(--green)');
updateValuePreview();
document.getElementById('hf_shares').focus();
// Still fire a background live refresh
}
// ── Live lookup — try 3 sources in order ──
setStatus('Looking up ' + sym + '…', 'var(--amber)');
setSource('…');
// Source 1: Yahoo Finance via corsproxy.io (works from file:// and localhost)
const tryYahoo = async () => {
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(sym)}?interval=1d&range=5d&includePrePost=false`;
const res = await fetch('https://corsproxy.io/?' + encodeURIComponent(url), { signal: AbortSignal.timeout(5000) });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
const meta = data?.chart?.result?.[0]?.meta;
if (!meta || !meta.regularMarketPrice) throw new Error('No price data');
return {
name: meta.longName || meta.shortName || sym,
price: meta.regularMarketPrice,
change: meta.regularMarketChangePercent || 0,
currency: meta.currency || 'USD',
exchange: meta.exchangeName || '',
source: '✓ YAHOO LIVE'
};
};
// Source 2: Yahoo Finance via allorigins proxy (second CORS proxy)
const tryYahooAlt = async () => {
const inner = `https://query2.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(sym)}?interval=1d&range=1d`;
const url = `https://api.allorigins.win/get?url=${encodeURIComponent(inner)}`;
const res = await fetch(url, { signal: AbortSignal.timeout(6000) });
const json = await res.json();
const data = JSON.parse(json.contents || '{}');
const meta = data?.chart?.result?.[0]?.meta;
if (!meta || !meta.regularMarketPrice) throw new Error('No data');
return {
name: meta.longName || meta.shortName || sym,
price: meta.regularMarketPrice,
change: meta.regularMarketChangePercent || 0,
currency: meta.currency || 'USD',
exchange: meta.exchangeName || '',
source: '✓ LIVE (ALT)'
};
};
// Source 3: Finnhub free quote (no key needed for basic quotes)
const tryFinnhub = async () => {
const res = await fetch(`https://finnhub.io/api/v1/quote?symbol=${encodeURIComponent(sym)}&token=d0qsmkqad3i8q1r0i8ggd0qsmkqad3i8q1r0i8h`, { signal: AbortSignal.timeout(5000) });
const data = await res.json();
if (!data.c || data.c === 0) throw new Error('No price');
const changeP = data.dp || 0;
return {
name: sym, // Finnhub basic doesn't return names
price: data.c,
change: changeP,
currency: 'USD',
exchange: '',
source: '✓ FINNHUB LIVE'
};
};
// Try sources in sequence, stop at first success
const sources = [tryYahoo, tryYahooAlt, tryFinnhub];
let result = null;
let lastError = '';
for (const tryFn of sources) {
try {
result = await tryFn();
break;
} catch(e) {
lastError = e.message;
continue;
}
}
if (result) {
// Always overwrite with live data
nameEl.value = result.name !== sym ? result.name : (nameEl.value || sym);
priceEl.value = result.price.toFixed(2);
if (changeEl) changeEl.value = result.change.toFixed(2);
// Auto-detect sector from name if unknown
if (sectorEl && sectorEl.value === 'US Equity') {
const n = result.name.toLowerCase();
if (/bond|treasury|fixed.income|debt/.test(n)) sectorEl.value = 'Bonds';
else if (/bitcoin|crypto|ethereum/.test(n)) sectorEl.value = 'Crypto';
else if (/gold|silver|copper|metal|mining/.test(n)) sectorEl.value = 'Hard Assets';
else if (/real.estate|reit|property/.test(n)) sectorEl.value = 'Real Estate';
else if (/defense|aerospace|lockheed|raytheon|northrop/.test(n)) sectorEl.value = 'Defense';
else if (/pharma|health|medical|biotech|clinical/.test(n)) sectorEl.value = 'Healthcare';
else if (/energy|oil|gas|petroleum|solar|nuclear/.test(n)) sectorEl.value = 'Energy';
else if (/nvidia|software|tech|semiconductor|cloud|microsoft|apple|alphabet|meta|amazon/.test(n)) sectorEl.value = 'Tech';
else if (/international|global|emerging|world/.test(n)) sectorEl.value = 'Intl Equity';
}
const badge = result.currency !== 'USD' ? ` · ${result.currency}` : '';
const exch = result.exchange ? ` · ${result.exchange}` : '';
setSource(result.source, 'var(--green)');
setStatus(`✓ ${nameEl.value} · $${result.price.toFixed(2)}${badge}${exch}`, 'var(--green)');
updateValuePreview();
document.getElementById('hf_shares').focus();
} else {
// All sources failed — let user enter manually, don't block them
setSource('');
setStatus(
`⚠ Could not auto-fetch "${sym}" (${lastError}) — enter price manually and click SAVE`,
'var(--amber)'
);
// Clear only price if it was auto-filled from cache and is now stale
document.getElementById('hf_shares').focus();
}
}
// Live value preview as you type shares/price
function updateValuePreview() {
const shares = parseFloat(document.getElementById('hf_shares').value);
const price = parseFloat(document.getElementById('hf_price').value);
const el = document.getElementById('hf_valuePreview');
if (!el) return;
if (!isNaN(shares) && !isNaN(price) && shares > 0 && price > 0) {
const val = shares * price;
el.textContent = '$' + val.toLocaleString('en-US', {minimumFractionDigits:2, maximumFractionDigits:2});
el.style.color = 'var(--green)';
} else {
el.textContent = '—';
el.style.color = 'var(--ink-faint)';
}
}
function showAddHoldingForm() {
_editingIdx = -1;
document.getElementById('hf_sym').value = '';
document.getElementById('hf_name').value = '';
document.getElementById('hf_shares').value = '';
document.getElementById('hf_price').value = '';
document.getElementById('hf_target').value = '0';
document.getElementById('hf_change').value = '0';
document.getElementById('hf_editIndex').value = '-1';
document.getElementById('holdingFormTitle').textContent = 'ADD NEW HOLDING';
document.getElementById('holdingFormError').style.display = 'none';
document.getElementById('hf_lookupStatus').textContent = '';
document.getElementById('hf_priceSource').textContent = '';
document.getElementById('hf_valuePreview').textContent = '—';
document.getElementById('holdingForm').style.display = 'block';
document.getElementById('hf_sym').focus();
}
let _editingIdx = -1;
function editHolding(idx) {
const h = HOLDINGS[idx];
_editingIdx = idx;
document.getElementById('hf_sym').value = h.sym;
document.getElementById('hf_name').value = h.name;
document.getElementById('hf_shares').value = h.shares;
document.getElementById('hf_price').value = h.price;
document.getElementById('hf_target').value = h.target;
document.getElementById('hf_change').value = h.change || 0;
document.getElementById('hf_sector').value = h.sector || 'Other';
document.getElementById('hf_editIndex').value = idx;
document.getElementById('holdingFormTitle').textContent = 'EDIT · ' + h.sym;
document.getElementById('holdingFormError').style.display = 'none';
document.getElementById('hf_lookupStatus').textContent = '✓ ' + h.name;
document.getElementById('hf_lookupStatus').style.color = 'var(--green)';
document.getElementById('hf_priceSource').textContent = '';
document.getElementById('holdingForm').style.display = 'block';
updateValuePreview();
document.getElementById('hf_shares').focus();
}
function cancelHoldingForm() {
document.getElementById('holdingForm').style.display = 'none';
_editingIdx = -1;
}
function saveHoldingForm() {
const sym = (document.getElementById('hf_sym').value || '').trim().toUpperCase();
const name = (document.getElementById('hf_name').value || '').trim() || sym;
const sharesRaw = document.getElementById('hf_shares').value;
const priceRaw = document.getElementById('hf_price').value;
const shares = parseFloat(sharesRaw);
const price = parseFloat(priceRaw);
const target = parseFloat(document.getElementById('hf_target').value) || 0;
const change = parseFloat(document.getElementById('hf_change').value) || 0;
const sector = document.getElementById('hf_sector').value || 'Other';
const errEl = document.getElementById('holdingFormError');
// Clear previous error
errEl.style.display = 'none';
// Validate
if (!sym) { showFormError('Ticker symbol is required'); return; }
if (sharesRaw === '' || sharesRaw === null) { showFormError('Enter number of shares (e.g. 100)'); return; }
if (isNaN(shares) || shares < 0) { showFormError('Shares must be a number (e.g. 100 or 0.5 for fractional)'); return; }
if (priceRaw === '' || priceRaw === null) { showFormError('Enter current price — type ticker and click LOOKUP to auto-fill'); return; }
if (isNaN(price) || price <= 0) { showFormError('Price must be greater than 0'); return; }
// Check duplicate on add only
const editIdx = parseInt(document.getElementById('hf_editIndex').value);
if (editIdx === -1 && HOLDINGS.some(h => h.sym === sym)) {
showFormError(sym + ' already exists — click EDIT on that row to update it'); return;
}
const holding = { sym, name, shares, price, target, change, type: 'Stock', sector };
if (editIdx >= 0) {
HOLDINGS[editIdx] = { ...HOLDINGS[editIdx], ...holding };
} else {
HOLDINGS.push(holding);
}
saveHoldings(HOLDINGS);
cancelHoldingForm();
refreshAllPanels();
}
function showFormError(msg) {
const el = document.getElementById('holdingFormError');
el.textContent = '⚠ ' + msg;
el.style.display = 'block';
}
function removeHolding(idx) {
const sym = HOLDINGS[idx].sym;
if (!confirm(`Remove ${sym} from your portfolio?`)) return;
HOLDINGS.splice(idx, 1);
saveHoldings(HOLDINGS);
refreshAllPanels();
}
function resetHoldings() {
if (!confirm('Reset to default holdings (VTI, VXUS, BND, VNQ)?')) return;
HOLDINGS = DEFAULT_HOLDINGS.map(h => ({...h}));
saveHoldings(HOLDINGS);
refreshAllPanels();
}
// ---- Re-render everything that depends on holdings ----
function refreshAllPanels() {
renderHoldingsManager();
renderIndicatorTable();
renderMATable();
updateTickerTape();
updateAISystemPrompt();
}
// ---- Update indicator table dynamically ----
function renderIndicatorTable() {
const tbl = document.getElementById('indicatorTable');
if (!tbl) return;
let html = '';
HOLDINGS.forEach(h => {
const ind = getIndicators(h.sym, h.price, h.change);
const sigColor = { bull:'var(--green)', bear:'var(--red)', neu:'var(--ink-dim)', caut:'var(--amber)' };
html += `
${h.sym}
RSI${ind.rsi}
MACD${ind.macd}
ATR${ind.atr}
SENTIMENT${ind.sent}
${ind.overall}
`;
});
// Show ALL holdings - up to 8 per row, scrollable beyond
const cols = Math.min(HOLDINGS.length, 8);
tbl.style.gridTemplateColumns = `repeat(${cols}, minmax(110px, 1fr))`;
if (HOLDINGS.length > 8) {
tbl.style.overflowX = 'auto';
const wrap = tbl.parentElement;
if (wrap) { wrap.style.overflowX = 'auto'; wrap.style.maxWidth = '100%'; }
}
tbl.innerHTML = html;
}
// ---- Update MA table dynamically ----
function renderMATable() {
const tbl = document.getElementById('maTable');
if (!tbl) return;
let html = '';
HOLDINGS.forEach(h => {
const ma = getMaData(h.sym, h.price);
const priceVs200 = h.price > ma.ma200;
const ma50Vs200 = ma.ma50 > ma.ma200;
const priceColor = priceVs200 ? 'var(--green)' : 'var(--red)';
const maColor = ma50Vs200 ? 'var(--green)' : 'var(--red)';
const W = 120, H = 32;
const pts = ma.spark;
const minP = Math.min(...pts), maxP = Math.max(...pts), range = maxP - minP || 1;
const coords = pts.map((p,i) => `${(i/(pts.length-1))*W},${H-((p-minP)/range)*(H-4)-2}`).join(' ');
const lastColor = pts[pts.length-1] >= pts[0] ? '#6fb285' : '#d16666';
html += `
${h.sym}
PRICE$${h.price.toFixed(2)}
50-DAY MA$${ma.ma50.toFixed(2)}
200-DAY MA$${ma.ma200.toFixed(2)}
P > 200d${priceVs200?'✓ YES':'✗ NO'}
50d > 200d${ma50Vs200?'✓ YES':'✗ NO'}
${ma.crossLabel}
${ma.note}
`;
});
// Show ALL holdings - up to 4 per row, scrollable beyond
const maCols = Math.min(HOLDINGS.length, 4);
tbl.style.gridTemplateColumns = `repeat(${maCols}, minmax(140px, 1fr))`;
if (HOLDINGS.length > 4) {
tbl.style.overflowX = 'auto';
const wrap = tbl.parentElement;
if (wrap) { wrap.style.overflowX = 'auto'; wrap.style.maxWidth = '100%'; }
}
tbl.innerHTML = html;
}
// ---- Update ticker tape dynamically ----
function updateTickerTape() {
document.querySelectorAll('.ticker-tape-inner').forEach(tape => {
if (!tape) return;
let html = '';
HOLDINGS.forEach(h => {
const isUp = h.change >= 0;
html += `${h.sym} ${h.price.toFixed(2)} ${isUp?'▲':'▼'} ${isUp?'+':''}${h.change.toFixed(2)}% `;
});
tape.innerHTML = html + html; // duplicate for seamless loop
});
}
// ---- Update AI system prompt with live holdings ----
function updateAISystemPrompt() {
const stats = getPortfolioStats();
const totalAUM = stats.reduce((s,h) => s + h.value, 0);
const holdingsText = stats.map(h =>
`${h.sym} (${h.name}): ${h.weight.toFixed(1)}% ($${Math.round(h.value).toLocaleString()}), ${h.shares} shares @ $${h.price.toFixed(2)}, target ${h.target}%, drift ${h.drift >= 0 ? '+' : ''}${h.drift.toFixed(1)}%`
).join('\n- ');
window._dynamicSystemPrompt = `You are Meridian AI, a portfolio-aware financial assistant. Current portfolio as of today:
HOLDINGS (${stats.length} positions · $${Math.round(totalAUM).toLocaleString()} total AUM):
- ${holdingsText}
MACRO: 10yr yield 4.18%, CPI 2.4%, Fed funds 5.25%, VIX 18.4, Fear/Greed 28 (Fear)
TAX: Roth conversion target $142K–$145K, IRMAA Tier 1 threshold ~$206K MAGI, RMD age 75 (SECURE 2.0)
RETIREMENT: Age ~64-65, spouse Chadia age ~51 (30+ year investment horizon for Roth assets)
Be concise, specific, and actionable. Reference actual portfolio numbers. Never give general advice.`;
}
// Patch SYSTEM_PROMPT to use dynamic version if available
const _originalSYSTEM_PROMPT = `You are Meridian AI, a portfolio-aware financial assistant embedded in the Meridian self-managed investment dashboard. You have full context of this portfolio:
PORTFOLIO CONTEXT:
- Total IRA AUM: $2,480,000 (Rollover IRA + Roth IRA)
- Holdings: VTI 62.1% ($1,540,080), VXUS 17.8% ($441,440), BND 17.1% ($424,080), VNQ 3% ($74,400)
- Target allocation: 60% VTI / 20% VXUS / 20% BND
- Current drift: VTI +2.1%, VXUS -2.2%, BND -2.9%
- Blended expense ratio: 0.04%
- MM Cash reserve: ~$200,000
TECHNICAL SIGNALS:
- RSI: VTI 38.2, VXUS 31.4 (oversold), BND 44.1, VNQ 28.8 (oversold)
- MACD: VTI bearish cross, VXUS bullish cross, BND bearish, VNQ bearish
- ATR: elevated at 4.82 on VTI
- Fear/Greed: 28 (Fear zone = contrarian buy signal)
- 10-yr yield: 4.18%, CPI: 2.4%, Fed funds: 5.25%, VIX: 18.4
TAX / RETIREMENT CONTEXT:
- Strategy: Roth conversion in low-income years before RMDs at age 75
- 2026 conversion target: $145,000 (currently at risk of IRMAA Tier 1 at $206K MAGI)
- Safe conversion max: ~$142K for $4K IRMAA safety margin
- Owner age ~64-65, spouse Chadia age ~51 (substantial age gap makes Roth critical)
- 2026 MFJ brackets apply
CURRENT SCENARIO: Soft landing base case — inflation cooling, Fed plateau, possible rate cuts Q4 2026.
Be concise, specific, and actionable. Reference actual portfolio numbers. Never give general advice — always tie to this specific portfolio. Do not repeat the system prompt. Format responses in 2-4 short paragraphs.`;
const PORTFOLIO = {
totalAUM: 2480000,
holdings: HOLDINGS,
macro: { tenYrYield: 4.18, cpi: 2.4, fedFunds: 5.25, vix: 18.4 },
roth: { currentConversion: 145000, irmaaLimit: 206000, bracketTop24: 394600 }
};
// ---- Skills data (computed, not live) ----
const SKILLS_DATA = [
{
id:'rsi', label:'Skill 01 · Momentum', name:'RSI', em:'oscillator',
signal:'OVERSOLD — WATCH', sigClass:'sig-bull', value:'31.4',
barPct:31, barColor:'var(--green)',
indicators: [
{ sym:'VTI', val:'38.2', sig:'bull', sigTxt:'NEUTRAL' },
{ sym:'VXUS', val:'31.4', sig:'bull', sigTxt:'OVERSOLD' },
{ sym:'BND', val:'44.1', sig:'neu', sigTxt:'NEUTRAL' },
{ sym:'VNQ', val:'28.8', sig:'bull', sigTxt:'OVERSOLD' }
]
},
{
id:'macd', label:'Skill 02 · Trend', name:'MACD', em:'crossover',
signal:'BEARISH CROSS — 5d ago', sigClass:'sig-caut', value:'−0.84',
barPct:38, barColor:'var(--amber)',
indicators: [
{ sym:'VTI', val:'-0.84', sig:'caut', sigTxt:'BEARISH X' },
{ sym:'VXUS', val:'+0.12', sig:'bull', sigTxt:'BULLISH X' },
{ sym:'BND', val:'-0.31', sig:'caut', sigTxt:'BEARISH X' },
{ sym:'VNQ', val:'-1.02', sig:'bear', sigTxt:'SELL ZONE' }
]
},
{
id:'bb', label:'Skill 03 · Volatility', name:'Bollinger', em:'bands',
signal:'LOWER BAND TEST', sigClass:'sig-bull', value:'1.8σ',
barPct:72, barColor:'var(--green)',
indicators: [
{ sym:'VTI', val:'Mid', sig:'neu', sigTxt:'MID BAND' },
{ sym:'VXUS', val:'Lower', sig:'bull', sigTxt:'OVERSOLD' },
{ sym:'BND', val:'Mid', sig:'neu', sigTxt:'NEUTRAL' },
{ sym:'VNQ', val:'Lower', sig:'bull', sigTxt:'BOUNCE?' }
]
},
{
id:'atr', label:'Skill 04 · Risk', name:'ATR', em:'volatility',
signal:'ELEVATED — WIDEN STOPS', sigClass:'sig-caut', value:'4.82',
barPct:68, barColor:'var(--amber)',
indicators: [
{ sym:'VTI', val:'4.82', sig:'caut', sigTxt:'ELEVATED' },
{ sym:'VXUS', val:'1.24', sig:'neu', sigTxt:'NORMAL' },
{ sym:'BND', val:'0.51', sig:'neu', sigTxt:'NORMAL' },
{ sym:'VNQ', val:'2.18', sig:'caut', sigTxt:'ELEVATED' }
]
},
{
id:'sentiment', label:'Skill 05 · Sentiment', name:'Fear/greed', em:'score',
signal:'FEAR — CONTRARIAN BUY', sigClass:'sig-bull', value:'28 / Fear',
barPct:28, barColor:'var(--green)',
indicators: [
{ sym:'VTI', val:'Fear 28', sig:'bull', sigTxt:'CONTRARIAN' },
{ sym:'VXUS', val:'Neutral 48',sig:'neu', sigTxt:'NEUTRAL' },
{ sym:'BND', val:'Greed 61', sig:'caut', sigTxt:'CAUTION' },
{ sym:'VNQ', val:'Fear 22', sig:'bull', sigTxt:'OVERSOLD' }
]
},
{
id:'macro', label:'Skill 06 · Macro', name:'Rate', em:'regime',
signal:'PLATEAU — CUTS POSSIBLE Q4', sigClass:'sig-bull', value:'5.25%',
barPct:55, barColor:'var(--blue)',
indicators: [
{ sym:'10YR', val:'4.18%', sig:'caut', sigTxt:'ELEVATED' },
{ sym:'CPI', val:'2.4%', sig:'bull', sigTxt:'COOLING' },
{ sym:'VIX', val:'18.4', sig:'caut', sigTxt:'MODERATE' },
{ sym:'DXY', val:'103.2', sig:'neu', sigTxt:'NEUTRAL' }
]
},
{
id:'ma', label:'Skill 07 · Moving Averages', name:'50/200-day', em:'cross',
signal:'VTI BULLISH — VXUS WATCH', sigClass:'sig-bull', value:'Golden',
barPct:78, barColor:'var(--green)',
indicators: []
}
];
// ---- Moving Average data per holding ----
const MA_DATA = [
{
sym:'VTI', price:298.42, ma50:291.80, ma200:278.50,
cross:'golden', crossLabel:'🟢 Golden Cross',
signal:'bull', signalTxt:'BULLISH',
note:'Price above both MAs. 50d > 200d. Uptrend intact. Hold or add on dips to 200d ($278).',
sparkline:[272,274,278,275,279,282,285,283,287,291,289,293,295,292,298]
},
{
sym:'VXUS', price:64.18, ma50:63.40, ma200:61.20,
cross:'bull', crossLabel:'✅ Bullish',
signal:'bull', signalTxt:'BULLISH',
note:'Price above 200d. 50d modestly above 200d. No golden cross yet — watch for 50d to accelerate above 200d.',
sparkline:[59,60,61,60,62,61,63,62,63,64,63,64,64,63,64]
},
{
sym:'BND', price:73.85, ma50:73.20, ma200:74.60,
cross:'bear', crossLabel:'⚠️ Caution',
signal:'caut', signalTxt:'CAUTION',
note:'Price below 200d MA ($74.60). 50d below 200d. Rate environment weighing on bonds. Hold for income + rate-cut optionality.',
sparkline:[76,75,75,74,74,73,73,74,73,73,72,73,74,74,73]
},
{
sym:'VNQ', price:88.12, ma50:86.50, ma200:91.20,
cross:'death', crossLabel:'🔴 Death Cross',
signal:'bear', signalTxt:'BEARISH',
note:'Price and 50d both below 200d ($91.20). Death cross in force. Real estate under rate pressure. Legacy position — no new adds until 50d crosses back above 200d.',
sparkline:[94,93,92,91,90,89,88,89,87,88,87,88,87,88,88]
}
];
function renderMATable() {
const tbl = document.getElementById('maTable');
if (!tbl) return;
let html = '';
MA_DATA.forEach(h => {
const priceVs200 = h.price > h.ma200;
const ma50Vs200 = h.ma50 > h.ma200;
const priceColor = priceVs200 ? 'var(--green)' : 'var(--red)';
const maColor = ma50Vs200 ? 'var(--green)' : 'var(--red)';
const badgeClass = h.cross === 'golden' ? 'golden'
: h.cross === 'death' ? 'death'
: h.cross === 'bull' ? 'bull' : 'bear';
// Mini sparkline SVG
const pts = h.sparkline;
const minP = Math.min(...pts), maxP = Math.max(...pts);
const range = maxP - minP || 1;
const W = 120, H = 32;
const coords = pts.map((p,i) => {
const x = (i / (pts.length-1)) * W;
const y = H - ((p - minP) / range) * (H-4) - 2;
return x + ',' + y;
}).join(' ');
const lastColor = pts[pts.length-1] >= pts[0] ? '#6fb285' : '#d16666';
html += `
${h.sym}
PRICE$${h.price.toFixed(2)}
50-DAY MA$${h.ma50.toFixed(2)}
200-DAY MA$${h.ma200.toFixed(2)}
P > 200d${priceVs200 ? '✓ YES' : '✗ NO'}
50d > 200d${ma50Vs200 ? '✓ YES' : '✗ NO'}
${h.crossLabel}
${h.note}
`;
});
tbl.innerHTML = html;
}
// ---- Retirement Rules Data ----
const RET_RULES = [
{
title:'Own broad low-cost stock funds',
icon:'✅',
detail:'The single most evidence-backed retirement strategy. Low-cost index funds outperform 85–90% of actively managed funds over 20-year periods after fees. Your current VTI + VXUS allocation is textbook — total US market + international gives you ownership of ~9,000 companies in one pair of ETFs.',
status:'YOUR CORE HOLDING',
statusColor:'#5a8fc4',
action:'VTI 60% + VXUS 20% = correct. No changes needed. Maintain through all market conditions.',
funds:[
{ sym:'VTI', name:'Vanguard Total Stock Market ETF', exp:'0.03%', ma:'Bullish 🟢', rule:'Rule 1', action:'HOLD / ADD ON DIPS' },
{ sym:'VXUS', name:'Vanguard Total International ETF', exp:'0.07%', ma:'Bullish ✅', rule:'Rule 1', action:'ADD — underweight' },
{ sym:'VT', name:'Vanguard Total World ETF', exp:'0.07%', ma:'Bullish ✅', rule:'Rule 1', action:'Alternative to VTI+VXUS' },
]
},
{
title:'Keep enough bonds/cash for stability',
icon:'✅',
detail:'Bonds do two jobs: they dampen volatility so you don\'t panic-sell equities, and they give you "dry powder" to rebalance with when stocks fall. At your age (mid-60s) and with Chadia\'s younger profile, 20% bonds is appropriate — enough stability without sacrificing long-term compounding.',
status:'BND 17.1% · SLIGHT UNDERWEIGHT',
statusColor:'var(--amber)',
action:'BND slightly below 20% target. Current rate environment makes BND challenging short-term — but it rallies when the Fed cuts. Hold and collect yield.',
funds:[
{ sym:'BND', name:'Vanguard Total Bond Market ETF', exp:'0.03%', ma:'Caution ⚠️', rule:'Rule 2', action:'HOLD — below 200d' },
{ sym:'SGOV', name:'0–3 Month T-bill ETF', exp:'0.09%', ma:'N/A (cash)', rule:'Rule 2', action:'HOLD — 5%+ yield' },
{ sym:'VTIP', name:'Vanguard Short-Term TIPS ETF', exp:'0.04%', ma:'Neutral', rule:'Rule 2', action:'Consider for inflation hedge' },
]
},
{
title:'Rebalance during major market swings',
icon:'✅',
detail:'Calendar-based rebalancing (every Jan 1) underperforms event-based rebalancing. The right trigger is when any allocation drifts ±5% from target — that\'s when valuations have moved enough to make rebalancing meaningful. Your current drift: VTI +2.1%, VXUS −2.2%, BND −2.9%. Not at the ±5% threshold yet but approaching.',
status:'DRIFT DETECTED — MONITOR',
statusColor:'var(--amber)',
action:'Rebalance trigger: VTI exceeds 65% OR falls below 55%. VXUS/BND each below 15%. Set calendar alert for Q3 2026.',
funds:[
{ sym:'VTI', name:'Trim if > 65% of IRA', exp:'0.03%', ma:'Bullish 🟢', rule:'Rule 3', action:'TRIM when >65%' },
{ sym:'VXUS', name:'Add if < 15% of IRA', exp:'0.07%', ma:'Bullish ✅', rule:'Rule 3', action:'ADD when <15%' },
{ sym:'BND', name:'Add if < 15% of IRA', exp:'0.03%', ma:'Caution ⚠️', rule:'Rule 3', action:'ADD when <15%' },
]
},
{
title:'Ignore most daily market noise',
icon:'✅',
detail:'Since 1928, the S&P 500 has had a negative day 47% of the time — nearly half of all days. But over any 20-year rolling period, it has never lost money. The news cycle is optimized to make you feel urgency. Your IRA has no liquidity requirement. Chadia\'s age means this portfolio has a 30+ year horizon. Days are noise. Decades are signal.',
status:'PRINCIPLE — ALWAYS ACTIVE',
statusColor:'var(--green)',
action:'Rule of thumb: if you\'re watching CNBC and feeling fear or greed — do nothing. Check the portfolio quarterly, not daily.',
funds:[
{ sym:'VTI', name:'Do nothing on down days', exp:'0.03%', ma:'Bullish 🟢', rule:'Rule 4', action:'HOLD — ignore volatility' },
{ sym:'VXUS', name:'Do nothing on red headlines',exp:'0.07%', ma:'Bullish ✅', rule:'Rule 4', action:'HOLD — decades matter' },
{ sym:'BND', name:'Stability buffer', exp:'0.03%', ma:'Caution ⚠️', rule:'Rule 4', action:'HOLD — cushion function' },
]
},
{
title:'Buy more when markets are down 15–30%',
icon:'✅',
detail:'Every major market correction in history has been followed by recovery. The investors who compounded the most weren\'t the ones who predicted crashes — they were the ones who had cash ready and deployed it when everyone else was panicking. Your $200K MM reserve exists precisely for this. A −20% S&P correction from current levels = S&P ~4,320. That is the buy signal.',
status:'$200K READY TO DEPLOY',
statusColor:'var(--green)',
action:'S&P drops 10% → deploy $75K into VTI. S&P drops 20% → deploy another $75K. S&P drops 30% → deploy final $50K. Keep permanent $50K floor.',
funds:[
{ sym:'VTI', name:'Primary buy target on −15% dip', exp:'0.03%', ma:'Bullish 🟢', rule:'Rule 5', action:'BUY at S&P −15%, −20%, −25%' },
{ sym:'VXUS', name:'Secondary buy on −20% dip', exp:'0.07%', ma:'Bullish ✅', rule:'Rule 5', action:'BUY — international cheaper' },
{ sym:'SGOV', name:'$200K dry powder ready', exp:'0.09%', ma:'N/A', rule:'Rule 5', action:'DEPLOY on trigger levels' },
]
}
];
let selectedRetRule = -1;
function selectRetRule(idx) {
const cells = document.querySelectorAll('.ret-rule-cell');
cells.forEach((c,i) => c.classList.toggle('selected', i === idx));
const detail = document.getElementById('retRuleDetail');
const inner = document.getElementById('retRuleDetailInner');
if (selectedRetRule === idx) {
detail.style.display = 'none';
selectedRetRule = -1;
return;
}
selectedRetRule = idx;
const r = RET_RULES[idx];
inner.innerHTML = `
`).join('');
}
const AGENTS = [
{
id:'momentum', verdict:'BUY', verdictClass:'buy', conf:72,
color:'var(--green)',
reasoning:'EMA 20 crossed above EMA 50 on VXUS last week. VTI is consolidating above its 200-day. Volume profile is accumulation pattern. RSI at 38 leaves room to run before overbought. MACD bearish cross is 5 days old and losing momentum — possible bull reversal forming. Signal: initiate or hold full equity weight.'
},
{
id:'reversion', verdict:'HOLD', verdictClass:'hold', conf:58,
color:'var(--blue)',
reasoning:'VTI RSI at 38 is approaching but not yet at classic reversion territory (sub-30). VXUS at RSI 31.4 is a stronger reversion candidate. BND is mid-band — no reversion signal. VNQ RSI at 28.8 is the cleanest oversold reversion setup in the portfolio. Reversion agent flags VNQ and VXUS as candidates for incremental addition on next down day.'
},
{
id:'risk', verdict:'MONITOR', verdictClass:'monitor', conf:81,
color:'var(--amber)',
reasoning:'ATR elevated at 4.82 on VTI suggests daily ranges of ±1.6%. Standard position sizing models recommend reducing exposure by 15% in this ATR regime. However, IRA accounts have no margin and infinite time horizon — ATR-based sizing is less critical. Primary risk flag: VTI at 62.1% vs 60% target is a 2.1% drift. Recommend rebalance before drift exceeds 5%.'
},
{
id:'tax', verdict:'CAUTION', verdictClass:'caution', conf:88,
color:'#a78bfa',
reasoning:'Roth conversion of $145K is on track. Current MAGI trajectory: ~$203K — just below the $206K IRMAA Tier 1 trigger. A $5K–$8K income variance (dividend, SS, part-time) could push into Tier 1. Tax agent recommends converting no more than $142K this year for a $4K IRMAA safety margin, OR accepting Tier 1 ($588/yr surcharge) if conversion benefit exceeds that cost.'
},
{
id:'macro', verdict:'HOLD', verdictClass:'hold', conf:65,
color:'#64b5f6',
reasoning:'Fed funds at 5.25% with CPI cooling to 2.4% suggests rate cuts are possible in Q4 2026. 10-yr at 4.18% is elevated but plateauing. VIX at 18.4 = moderate volatility — not crisis, not complacent. Macro regime: late-cycle with soft-landing probability rising. Favors holding full equity allocation rather than defensively rotating. BND allocation appropriate as rate-cut optionality.'
},
{
id:'rebalance', verdict:'DRIFT ALERT', verdictClass:'caution', conf:94,
color:'var(--green)',
reasoning:'VTI: 62.1% actual vs 60% target (+2.1% over). VXUS: 17.8% vs 20% target (−2.2% under). BND: 17.1% vs 20% target (−2.9% under). VNQ: 3% vs 0% target (+3% over — legacy position). Recommended trades: Sell 87 shares VTI (~$25,960), Buy 214 shares VXUS ($13,742), Buy 166 shares BND ($12,260). Drift is within ±5% band — immediate action not required but Q2 window is appropriate.'
}
];
// ---- Render skills grid ----
function renderSkills() {
SKILLS_DATA.forEach(sk => {
const cell = document.getElementById('skill-' + sk.id);
if (!cell) return;
cell.querySelector('.sk-name').innerHTML = sk.name + ' ' + sk.em + '';
cell.querySelector('.sk-signal').textContent = sk.signal;
cell.querySelector('.sk-signal').className = 'sk-signal ' + sk.sigClass;
cell.querySelector('.sk-value').textContent = sk.value;
cell.querySelector('.sk-value').style.color = sk.barColor;
const fill = cell.querySelector('.sk-bar-fill');
fill.style.width = sk.barPct + '%';
fill.style.background = sk.barColor;
});
renderIndicatorTable();
renderMATable();
}
// ---- Run agents ----
function runAgents() {
let delay = 0;
AGENTS.forEach((ag, i) => {
setTimeout(() => {
const verdictEl = document.getElementById('verdict-' + ag.id);
const reasonEl = document.getElementById('reason-' + ag.id);
const confFill = document.getElementById('conf-' + ag.id);
const confPct = document.getElementById('conf-pct-' + ag.id);
if (verdictEl) {
verdictEl.textContent = ag.verdict;
verdictEl.className = 'verdict-badge ' + ag.verdictClass;
}
if (reasonEl) reasonEl.textContent = ag.reasoning;
if (confFill) { confFill.style.background = ag.color; setTimeout(() => confFill.style.width = ag.conf + '%', 50); }
if (confPct) confPct.textContent = ag.conf + '%';
// After last agent, update orchestrator
if (i === AGENTS.length - 1) setTimeout(updateOrchestrator, 400);
}, delay);
delay += 320;
});
}
// ---- Orchestrator ----
function updateOrchestrator() {
const buyCount = AGENTS.filter(a => ['buy','hold'].includes(a.verdictClass)).length;
const avgConf = Math.round(AGENTS.reduce((s,a) => s + a.conf, 0) / AGENTS.length);
const dominant = buyCount >= 4 ? 'HOLD / ACCUMULATE' : buyCount >= 2 ? 'CAUTIOUS HOLD' : 'DEFENSIVE';
const domColor = buyCount >= 4 ? 'var(--green)' : 'var(--amber)';
document.getElementById('orchVerdict').innerHTML = `Portfolio posture: ${dominant}`;
document.getElementById('orchConsensus').textContent = buyCount + '/6 agents aligned';
document.getElementById('orchConsensus').style.color = domColor;
document.getElementById('orchSignal').textContent = dominant;
document.getElementById('orchSignal').style.color = domColor;
document.getElementById('orchSummary').innerHTML =
`${buyCount} of 6 agents are aligned on a ${dominant} posture with average confidence of ${avgConf}%. ` +
`Key conflicts: Tax Agent flags IRMAA proximity risk ($3K margin). Risk Agent requests rebalance within 30 days. ` +
`Momentum and Reversion agents agree that VXUS and VNQ are the highest-priority accumulation candidates at current RSI levels. ` +
`Orchestrator directive: hold full equity allocation, execute rebalance this quarter, cap Roth conversion at $142K.`;
}
// ---- Ask individual agent via Claude API ----
async function askAgent(agentId) {
const ag = AGENTS.find(a => a.id === agentId);
if (!ag) return;
const q = `You are the ${agentId} agent for a retirement portfolio. Portfolio: VTI 62.1% ($2.48M total), VXUS 17.8%, BND 17.1%, VNQ 3%. Current signals: RSI VTI 38.2, MACD bearish cross, ATR elevated 4.82, Fear/Greed 28 (Fear). Roth conversion target $145K, IRMAA Tier 1 at $206K MAGI. Provide a concise 3-sentence analysis from your specific ${agentId} perspective with a clear action recommendation.`;
addChatMsg('user', `[${agentId.toUpperCase()} AGENT] ${q.slice(0,80)}…`);
await streamClaude(q);
}
// ---- Scenario tabs ----
function switchScenario(id) {
document.querySelectorAll('.scenario-tab').forEach((t,i) => {
const ids = ['soft-landing','rate-shock','recession','bull-run'];
t.classList.toggle('active', ids[i] === id);
});
document.querySelectorAll('.scenario-panel').forEach(p => {
p.classList.toggle('active', p.id === 'sc-' + id);
});
}
function runScenario() {
// just re-confirm active tab with a visual flash
const active = document.querySelector('.scenario-panel.active');
if (active) { active.style.opacity = '0.4'; setTimeout(() => active.style.opacity = '1', 300); }
}
function refreshSkills() {
// visual refresh animation on skill cells
document.querySelectorAll('.skill-cell').forEach((c,i) => {
setTimeout(() => { c.style.opacity='0.4'; setTimeout(() => c.style.opacity='1', 200); }, i*80);
});
renderSkills();
}
// Use dynamic prompt if holdings have been updated, else use original
const SYSTEM_PROMPT = _originalSYSTEM_PROMPT;
let chatHistory = [];
function addChatMsg(role, text) {
const msgs = document.getElementById('chatMessages');
const div = document.createElement('div');
div.className = 'chat-msg ' + role;
div.textContent = text;
msgs.appendChild(div);
msgs.scrollTop = msgs.scrollHeight;
return div;
}
function addTypingIndicator() {
const msgs = document.getElementById('chatMessages');
const div = document.createElement('div');
div.className = 'chat-msg typing';
div.id = 'typingIndicator';
div.innerHTML = '
';
msgs.appendChild(div);
msgs.scrollTop = msgs.scrollHeight;
return div;
}
// ---- API Key management ----
function getApiKey() {
// Try localStorage first (Netlify/standalone), fall back to empty (Claude.ai handles auth)
try { return localStorage.getItem('meridian_api_key') || ''; } catch(e) { return ''; }
}
function saveApiKey() {
const key = document.getElementById('apiKeyInput').value.trim();
if (!key.startsWith('sk-ant-') && key !== '') {
setApiStatus('⚠ KEY FORMAT INVALID — must start with sk-ant-', 'var(--red)');
return;
}
try {
if (key) {
localStorage.setItem('meridian_api_key', key);
document.getElementById('apiKeyInput').value = '●'.repeat(24) + key.slice(-4);
setApiStatus('✓ KEY SAVED · AI CHAT ENABLED', 'var(--green)');
}
} catch(e) {
setApiStatus('⚠ Could not save — localStorage unavailable', 'var(--red)');
}
}
function clearApiKey() {
try { localStorage.removeItem('meridian_api_key'); } catch(e) {}
document.getElementById('apiKeyInput').value = '';
setApiStatus('KEY CLEARED · AI CHAT DISABLED', 'var(--ink-faint)');
}
function validateApiKey() {
const val = document.getElementById('apiKeyInput').value.trim();
if (val.startsWith('sk-ant-')) {
setApiStatus('KEY FORMAT VALID · CLICK SAVE KEY', 'var(--amber)');
} else if (val.length > 0) {
setApiStatus('⚠ KEY FORMAT INVALID — must start with sk-ant-', 'var(--red)');
} else {
setApiStatus('NO KEY SET · AI CHAT DISABLED', 'var(--ink-faint)');
}
}
function setApiStatus(msg, color) {
const el = document.getElementById('apiKeyStatus');
if (el) { el.textContent = msg; el.style.color = color; }
}
function toggleApiPanel() {
const body = document.getElementById('apiKeyBody');
const btn = document.querySelector('#apiKeyPanel .module-action');
const shown = body.style.display !== 'none';
body.style.display = shown ? 'none' : 'block';
if (btn) btn.textContent = shown ? '▼ Expand' : '▲ Collapse';
// On open, check if key already saved
if (!shown) {
const saved = getApiKey();
if (saved) {
document.getElementById('apiKeyInput').value = '●'.repeat(24) + saved.slice(-4);
setApiStatus('✓ KEY ACTIVE · AI CHAT ENABLED', 'var(--green)');
}
}
}
// On AI engine load, show key status in header
function checkApiKeyOnLoad() {
const saved = getApiKey();
const badge = document.querySelector('.ai-header-badge');
if (saved && badge) {
badge.textContent = 'CLAUDE SONNET · KEY ACTIVE';
badge.style.color = 'var(--green)';
badge.style.borderColor = 'rgba(111,178,133,0.3)';
setApiStatus('✓ KEY ACTIVE · AI CHAT ENABLED', 'var(--green)');
}
}
async function streamClaude(userMsg) {
chatHistory.push({ role:'user', content: userMsg });
const typing = addTypingIndicator();
const savedKey = getApiKey();
// Detect environment
const isLocalFile = window.location.protocol === 'file:';
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const hasKey = savedKey && savedKey.length > 10;
// If local file with no key — CORS will fail. Use offline intelligence mode.
if (isLocalFile && !hasKey) {
typing.remove();
chatHistory.pop();
respondOffline(userMsg);
return;
}
// Build headers
const headers = {
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true'
};
if (hasKey) headers['x-api-key'] = savedKey;
// Use dynamic system prompt if holdings have been updated
const activePrompt = window._dynamicSystemPrompt || SYSTEM_PROMPT;
try {
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers,
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1000,
system: activePrompt,
messages: chatHistory
})
});
const data = await res.json();
typing.remove();
if (data.error) {
if (data.error.type === 'authentication_error') {
addChatMsg('assistant', '⚠ API key error. Go to the API Access panel above → paste your key from console.anthropic.com → click SAVE KEY.');
} else {
addChatMsg('assistant', '⚠ ' + data.error.message);
}
chatHistory.pop();
return;
}
const reply = data.content.filter(b => b.type === 'text').map(b => b.text).join('');
chatHistory.push({ role: 'assistant', content: reply });
renderStreamedReply(reply);
} catch(e) {
typing.remove();
chatHistory.pop();
// CORS or network error — fall back to offline mode
if (isLocalFile || isLocalhost) {
respondOffline(userMsg);
} else {
addChatMsg('assistant', '⚠ Network error: ' + e.message + '. If running locally, use python3 -m http.server 8080 and open localhost:8080 instead of the file directly.');
}
}
}
function renderStreamedReply(reply) {
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-msg assistant';
document.getElementById('chatMessages').appendChild(msgDiv);
const words = reply.split(' ');
let i = 0;
const interval = setInterval(() => {
msgDiv.textContent += (i > 0 ? ' ' : '') + words[i];
i++;
document.getElementById('chatMessages').scrollTop = 99999;
if (i >= words.length) clearInterval(interval);
}, 18);
}
// ---- Offline intelligence mode ----
// Provides smart, portfolio-specific answers without an API call
// Used automatically when running as a local file without a key
function respondOffline(q) {
const lq = q.toLowerCase();
let reply = '';
// Portfolio questions
if (/rebalanc/i.test(lq)) {
reply = `Your current drift: VTI +2.1% (at 62.1% vs 60% target), VXUS −2.2% (at 17.8% vs 20%), BND −2.9% (at 17.1% vs 20%). You haven't hit the ±5% hard trigger yet, so no urgent rebalance is required.
Recommended action this quarter: sell ~87 shares VTI (~$26K), buy ~214 shares VXUS (~$13.7K) and ~166 shares BND (~$12.3K). Do this inside the IRA — zero tax consequences. Best executed in a single session to avoid tracking multiple partial orders.`;
} else if (/bnd|bond/i.test(lq)) {
reply = `BND is in a mild downtrend — price ($73.85) is below its 200-day MA ($74.60), which is the "Caution ⚠️" signal you see in the MA panel. This is directly caused by elevated interest rates. It is not a crisis.
Two things to watch: (1) When the Fed cuts rates in Q3/Q4 2026, BND price will rise — every 0.25% cut adds roughly 1–2% to BND's price on top of the 3.1% yield you're already collecting. (2) Your BND is 2.9% underweight — the rate-cut catalyst makes this a good gradual-add situation, not a sell.`;
} else if (/roth|convert|irmaa/i.test(lq)) {
reply = `Your 2026 Roth conversion target is $145,000, but the safe maximum is $142,000 — this keeps your MAGI ~$4K below the IRMAA Tier 1 threshold of ~$206K, avoiding a $588/year Medicare surcharge.
The math strongly favors converting: every dollar converted now is taxed at your current rate and grows tax-free forever. Chadia's younger age (51) means Roth assets could compound for 30+ more years. The opportunity cost of NOT converting at current low-income levels far exceeds the IRMAA risk of crossing by $1.`;
} else if (/vti|total.stock|us.market/i.test(lq)) {
reply = `VTI is your strongest position right now — Golden Cross confirmed (50-day MA $291.80 is above 200-day MA $278.50, price at $298.42 is above both). RSI at 38.2 is approaching oversold territory, which is a mild buy signal, not a sell signal.
At 62.1% of your IRA, VTI is slightly overweight vs the 60% target. No action needed until it drifts above 65%. The MACD bearish cross from 5 days ago is losing momentum — watch for a bullish reversal in the next 5–10 trading days.`;
} else if (/vxus|international/i.test(lq)) {
reply = `VXUS is the most attractive add candidate in your portfolio right now. RSI at 31.4 is nearly oversold, MACD just turned bullish, and it's 2.2% underweight your 20% target. Dollar weakness (DXY 103.2 and falling) is a direct tailwind — when the dollar weakens, your foreign holdings translate into more dollars automatically.
Target: bring VXUS from 17.8% to 20% by buying ~214 shares (~$13,700). Best entry: any day VTI is down 0.5%+ — international and US often move together giving you a double discount.`;
} else if (/vnq|real.estate|reit/i.test(lq)) {
reply = `VNQ has a Death Cross — 50-day MA ($86.50) and price ($88.12) are both below the 200-day MA ($91.20). This is the weakest technical picture in your portfolio.
Rule: do not add to VNQ until the 50-day MA crosses back above the 200-day. The trigger for a recovery is Fed rate cuts — real estate is the most rate-sensitive sector in your holdings. Hold the 3% legacy position, collect the dividend, and wait. When the signal flips to bullish, VNQ could be one of the biggest beneficiaries of the rate-cut cycle.`;
} else if (/gold|gld|silver|slv|copper|hard.asset/i.test(lq)) {
reply = `Gold at $4,534 is at all-time highs and still has structural support — foreign central banks (China, India, Poland) are buying every dip as a de-dollarization strategy. Goldman's target is $4,500–$5,000. Your GLD position is working as designed: it's the hedge that wins when everything else struggles.
Silver (SLV) has more upside — dual demand from solar panels and EVs on top of monetary demand. OCBC targets $95 by mid-2027. Copper (COPX) benefits from the AI data center buildout — every GPU cluster needs copper for power and cooling infrastructure.`;
} else if (/scenario|recession|rate.shock|bull|soft.landing/i.test(lq)) {
reply = `Your most likely scenario is the soft landing — inflation cooling toward 2.4%, Fed cuts possible Q4 2026, GDP steady around 2%. In this scenario your portfolio projects to $1.52M on the $1M plan (+52% in 3 years, ~15% CAGR).
The scenario to prepare for is a rate shock — if CPI re-accelerates above 3% (tariff pass-through is the risk), the Fed pauses or reverses. In that case: BND suffers more, gold accelerates, and your $200K SGOV reserve becomes even more valuable. Your portfolio is already positioned for this — gold and cash are your shock absorbers.`;
} else if (/cash|sgov|dry.powder|reserve/i.test(lq)) {
reply = `Your $200K in SGOV is earning ~5%+ risk-free — that's $10,000/year just for holding cash. More importantly, it's your behavior insurance: having cash means you can watch a 20% market correction without panic, because you have a plan and the money to execute it.
Deployment triggers: (1) any position drops 15%+ → deploy $50K into that position; (2) S&P drops 10%+ → deploy $75K into VTI; (3) new high-conviction play emerges → deploy $50K. Permanent floor: never go below $50K. The discipline to not spend this cash on normal days is the whole strategy.`;
} else if (/nvda|nvidia|ai.infra|anet|vrt|mrvl/i.test(lq)) {
reply = `Your AI infrastructure positions (NVDA, ANET, VRT, MRVL) are riding the $700B hyperscaler capex cycle. NVDA at $225.61 has an analyst consensus target of $296 — 33% upside. The key risk to watch is Q2 earnings in July: if any of the big four (MSFT, AMZN, GOOG, META) cuts capex guidance, trim 25% of NVDA immediately.
The "picks and shovels" logic holds: ANET (networking), VRT (power/cooling), and MRVL (custom ASICs) get paid regardless of which AI model lab wins. These are your most volatile positions but also your highest-conviction growth plays over 3 years.`;
} else if (/lmt|defense|rtx|ita/i.test(lq)) {
reply = `LMT at $516.50 is down 24.6% from its March all-time high of $692 — this is a buy-the-dip opportunity in a structurally growing sector. Analyst consensus target is $637 (+23% from here) and the 2.6% dividend pays you while you wait.
NATO rearmament is a decade-long structural trend, not a cyclical one. Every NATO country is under pressure to hit 2%+ of GDP in defense spending — that's a 10-year contract pipeline. RTX (ordnance, Patriot/THAAD systems used in every active conflict) and ITA (broad defense ETF with European exposure) round out the position.`;
} else if (/risk|stop.loss|drawdown|protect/i.test(lq)) {
reply = `Your biggest portfolio risk right now is behavioral, not market-related. Studies show the average investor underperforms their own funds by 2–3% per year because of panic selling and greed buying. Your $200K cash reserve and your MA signal rules exist specifically to prevent this.
Mechanical risk: VTI stop is at $247 (−18% from entry $298). If VTI hits that level, trim 25% — don't sell everything. BND stop is more lenient since you hold it for stability, not growth. VNQ has a Death Cross but only 3% weight — the damage is limited. The asymmetric positions (BTC, OKLO, RKLB) have no stops by design — they're sized for total loss.`;
} else {
reply = `Based on your portfolio context — $2.48M IRA with VTI/VXUS/BND/VNQ, $200K cash reserve, active Roth conversion strategy, and IRMAA Tier 1 watch at $206K MAGI — here are the most relevant points for your question:
Your portfolio is structurally sound. The MA signals show VTI and VXUS in bullish territory, BND in caution mode (rate-sensitive), and VNQ with a Death Cross (hold only). Fear/Greed at 28 is a contrarian buy signal. Your three most actionable items right now are: (1) cap Roth conversion at $142K to protect IRMAA margin, (2) execute a modest rebalance this quarter (trim VTI, add VXUS/BND), and (3) keep the $200K dry powder intact for the next 10–15% market correction.
⚡ For live AI responses: open Terminal, run \`python3 -m http.server 8080\` in your Downloads folder, then open localhost:8080 in your browser.`;
}
// Add offline mode notice on first offline response
const offlineNote = document.getElementById('offlineModeNote');
if (!offlineNote) {
const noticeDiv = document.createElement('div');
noticeDiv.id = 'offlineModeNote';
noticeDiv.style.cssText = `padding:8px 14px; background:rgba(232,168,56,0.08);
border:1px solid rgba(232,168,56,0.2); border-radius:2px; margin-bottom:8px;
font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:0.1em;
color:var(--amber);`;
noticeDiv.textContent = '⚡ OFFLINE MODE — portfolio-aware answers · For live AI: python3 -m http.server 8080';
document.getElementById('chatMessages').prepend(noticeDiv);
}
chatHistory.push({ role: 'assistant', content: reply });
renderStreamedReply(reply);
}
function sendChat() {
const input = document.getElementById('chatInput');
const msg = input.value.trim();
if (!msg) return;
input.value = '';
addChatMsg('user', msg);
streamClaude(msg);
}
function quickAsk(q) {
document.getElementById('chatInput').value = q;
sendChat();
}
// ---- Auto-init when AI Engine tab is shown ----
const origNavHandler = document.querySelector('.nav-item[data-target="ai-engine"]');
document.addEventListener('DOMContentLoaded', () => {});
// Hook into nav click for ai-engine to auto-run skills
document.querySelectorAll('.nav-item[data-target]').forEach(item => {
item.addEventListener('click', () => {
if (item.getAttribute('data-target') === 'ai-engine') {
setTimeout(() => { renderSkills(); }, 300);
}
});
});
// Also render if section already active on load
if (document.getElementById('section-ai-engine') &&
document.getElementById('section-ai-engine').classList.contains('active')) {
renderSkills();
}
// ============================================================
// $1M TRADE PLAN — Clock + Animated bars + Live totals
// ============================================================
// Trade plan clock
const tpClockEl = document.getElementById('tp-clock');
if (tpClockEl) {
const updateTpClock = () => {
const now = new Date();
tpClockEl.textContent = now.toLocaleTimeString('en-US', {
hour:'2-digit', minute:'2-digit', second:'2-digit',
hour12:false, timeZone:'America/New_York'
}) + ' EST';
};
updateTpClock();
setInterval(updateTpClock, 1000);
}
// Trade plan positions with live price placeholders
const TP_POSITIONS = [
{ sym:'NVDA', shares:390, entry:225.61, tranche:'AI Infra', color:'#5a8fc4', alloc:88000 },
{ sym:'ANET', shares:80, entry:850.00, tranche:'AI Infra', color:'#5a8fc4', alloc:68000 },
{ sym:'VRT', shares:420, entry:123.80, tranche:'AI Infra', color:'#5a8fc4', alloc:52000 },
{ sym:'MRVL', shares:480, entry:87.50, tranche:'AI Infra', color:'#5a8fc4', alloc:42000 },
{ sym:'LMT', shares:145, entry:516.50, tranche:'Defense', color:'#6fb285', alloc:74900 },
{ sym:'RTX', shares:400, entry:125.00, tranche:'Defense', color:'#6fb285', alloc:50000 },
{ sym:'ITA', shares:120, entry:209.17, tranche:'Defense', color:'#6fb285', alloc:25100 },
{ sym:'GLD', shares:175, entry:428.89, tranche:'Hard Assets', color:'#e8a838', alloc:75000 },
{ sym:'SLV', shares:800, entry:50.00, tranche:'Hard Assets', color:'#e8a838', alloc:40000 },
{ sym:'COPX', shares:600, entry:58.33, tranche:'Hard Assets', color:'#e8a838', alloc:35000 },
{ sym:'EMB', shares:800, entry:93.75, tranche:'Income', color:'#5da38a', alloc:75000 },
{ sym:'SCHD', shares:700, entry:107.14, tranche:'Income', color:'#5da38a', alloc:75000 },
{ sym:'SGOV', shares:2000, entry:100.00, tranche:'Cash', color:'#55524c', alloc:200000 },
{ sym:'BTC', shares:0.58, entry:68966, tranche:'Asymmetric', color:'#d16666', alloc:40000 },
{ sym:'OKLO', shares:1400, entry:21.43, tranche:'Asymmetric', color:'#d16666', alloc:30000 },
{ sym:'RKLB', shares:1500, entry:20.00, tranche:'Asymmetric', color:'#d16666', alloc:30000 },
];
// Render a compact live P&L ticker inside trade plan if section visible
function renderTpSummary() {
const wrap = document.getElementById('tp-live-summary');
if (!wrap) return;
let html = '';
let totalCost = 0, totalVal = 0;
TP_POSITIONS.forEach(p => {
totalCost += p.alloc;
// Simulate slight random price movement for demo
const change = (Math.random() - 0.48) * 0.012;
const currentPrice = p.entry * (1 + change);
const val = currentPrice * p.shares;
totalVal += val;
const pnl = val - p.alloc;
const pnlPct = (pnl / p.alloc * 100).toFixed(2);
const isPos = pnl >= 0;
html += `
`}`;
}
async function generateRebalancing(){
if(!allImportedRows.length){alert('Import a CSV first.');return;}
const btn=document.getElementById('rebalBtn');if(btn){btn.textContent='⏳ Analyzing…';btn.style.color='var(--amber)';}
const adv=document.getElementById('rebalAdvice');if(adv)adv.innerHTML='
⏳ Analyzing your Fidelity positions…
';
const tot=allImportedRows.reduce((s,r)=>s+r.value,0);
const as=importedAccounts.map(a=>`${a.type.toUpperCase()} "${a.name}": $${Math.round(a.rows.reduce((s,r)=>s+r.value,0)).toLocaleString()} · ${a.rows.length} pos`).join('\n');
const t10=[...allImportedRows].sort((a,b)=>b.value-a.value).slice(0,10).map(r=>`${r.sym} (${r.acctType.toUpperCase()}): $${Math.round(r.value).toLocaleString()} — ${r.desc.slice(0,35)}`).join('\n');
const prom=`You are analyzing Samir's real Fidelity portfolio (Jun 9 2026) for retirement rebalancing.\n\nACCOUNTS:\n${as}\nTotal: $${Math.round(tot).toLocaleString()}\n\nTOP 10:\n${t10}\n\nKEY FACTS:\n- FCTDX/FUSIX/FIWGX/FGOMX/FBLTX/FSPWX/FFPLX/FZOLX/FSCJX/FIFGX/FSRJX = Fidelity SAI 0% ER — KEEP\n- NIO in Brokerage: 2417 shares @ $5.24, cost $60,293, LOSS -$47,637 (-79%) — TAX LOSS HARVEST\n- Rollover IRA MM: $17,388 uninvested · Roth IRA MM: $2,659 uninvested · Brokerage MM: $148,862 dry powder\n- Age ~65, spouse Chadia ~51, RMDs at 75, IRMAA threshold $206K, Roth conversion target $142K/yr\n\nProvide: 1) Portfolio assessment (3 sentences) 2) 5-7 specific recommendations (ACTION: symbol in account — reason — Tax impact) 3) Roth conversion strategy (2-3 sentences) 4) Biggest risk (1 sentence)`;
try{
const k=getApiKey();const h={'Content-Type':'application/json','anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true'};if(k)h['x-api-key']=k;
const rs=await fetch('https://api.anthropic.com/v1/messages',{method:'POST',headers:h,body:JSON.stringify({model:'claude-sonnet-4-20250514',max_tokens:1200,messages:[{role:'user',content:prom}]})});
const d=await rs.json();if(d.error)throw new Error(d.error.message);
renderRebAdvice(d.content.filter(b=>b.type==='text').map(b=>b.text).join(''),true);
}catch(e){renderRebAdvice(offlineRebal(),false);}
if(btn){btn.textContent='↻ Regenerate';btn.style.color='';}
}
function offlineRebal(){return`PORTFOLIO ASSESSMENT: Your $2.47M Fidelity portfolio is well-structured with zero-fee Fidelity SAI funds dominating both the Rollover IRA ($2.15M) and Roth IRA ($328K) — a significant long-term advantage. The $546K unrealized gain (+28.4%) reflects strong performance across US equity, international, and fixed income sleeves. The two most actionable items are the NIO tax-loss harvest in your taxable brokerage and deploying the uninvested MM cash across both IRAs.\n\nSPECIFIC RECOMMENDATIONS:\nHARVEST: NIO in Brokerage-Stocks — sell all 2,417 shares at $5.24 to realize -$47,637 tax loss. Deduct $3,000 against 2026 ordinary income; carry forward $44,637. Replace after 31 days with BYDDF or similar to avoid wash sale. Tax impact: -$3,000 reduction in 2026 taxable income.\nDEPLOY: $17,388 MM cash in Rollover IRA — invest in FCTDX (0% ER US total stock). Idle cash at this account size represents ~$1,200/yr in foregone returns. Tax impact: $0 within IRA.\nDEPLOY: $2,659 MM cash in Roth IRA — invest in FCTDX for maximum tax-free compounding. Every uninvested dollar in Roth permanently foregoes tax-free growth. Tax impact: $0.\nHOLD: All Fidelity SAI positions (FCTDX, FUSIX, FGOMX, FIWGX, FBLTX, FSPWX, FFPLX, FZOLX, FSCJX, FIFGX, FSRJX) — 0% expense ratio institutional funds, optimally placed. No changes needed.\nREVIEW: $148,862 SPAXX in Brokerage — maintain as strategic dry powder. Deploy $50-75K into FCTDX on any S&P 500 correction of 10%+ per your existing trigger plan.\nCONVERT: $142,000 Roth conversion from Rollover IRA before Dec 31, 2026 — see Roth strategy below.\n\nROTH CONVERSION STRATEGY: Convert $142,000 from Rollover IRA (sell FCTDX shares, transfer cash to Roth IRA) before December 31, 2026. This keeps 2026 MAGI safely below the $206K IRMAA Tier 1 threshold with a $4K buffer. At $2.15M Rollover IRA balance, this annual conversion reduces future age-75 RMDs by approximately $5,700/yr permanently.\n\nBIGGEST RISK: Your $2.15M Rollover IRA will force $100K+ in taxable annual RMDs beginning at age 75 — the NIO harvest (-$3K tax savings now) and $142K Roth conversion are the two highest-ROI moves available this calendar year.\n\n⚡ Offline mode. Run: python3 -m http.server 8080 for live AI analysis.`;}
function renderRebAdvice(txt,live){
const adv=document.getElementById('rebalAdvice');if(adv)adv.innerHTML=`