Everything recomputes live. Close the panel and the client sees only the finished dashboard.
Sharing
The client password unlocks the client-facing view only. The advisor password unlocks that same view plus Personalize & Customize. Share each with the right audience.
New Client: The 4 Questions
1. What company are you concentrated in?
2. What do you have today?
3. What's the rest of your portfolio?
4. How do we seed the DI engine?
Funding can come from cash on hand or RSU sales.
Prices and company names auto-fill for common tickers (snapshot: Jul 13, 2026 close, always editable). Realistic tax lots are generated automatically.
Import Real Tax Lots (CSV)
Load the client's actual custodian export: Schwab, Fidelity, Morgan Stanley formats auto-detected. Parsing happens entirely in this window; nothing is uploaded anywhere.
Client & Scenario
Tax & Model Assumptions
Levers
Set the selectable pill values for each lever (comma-separated). $0 tax, $0 seed, and Off are fixed baselines and always shown.
Share Prices
Change a price to reprice every lot at once. × removes a ticker and all of its lots.
Lot Generator
Generates realistic lots at any volume. Fine-tune any row on The Details tab.
Export Client Build
Bakes everything above \u2014 client data, tax lots, both passwords \u2014 into one downloadable HTML file. Nothing in it depends on this browser\u2019s storage, so it opens correctly wherever it\u2019s hosted. This is the file to send to engineering for the client\u2019s site.
Always export from the current master template, not from a previously exported client file \u2014 otherwise template-wide fixes (fees, disclosures, etc.) won\u2019t carry over.
Demo Management
tag.
var seedJson = JSON.stringify(seed).replace(/\n' + clone.outerHTML;
}
function exportClientBuild(){
fetch(location.href, {cache:'no-store'}).then(function(r){
if(!r.ok) throw new Error('HTTP ' + r.status);
return r.text();
}).then(downloadClientBuildHtml).catch(function(err){
console.warn('exportClientBuild: could not re-fetch this page\u2019s source (' + err.message + '); falling back to a cleaned live-DOM snapshot. Embedded assets should still be intact, but formatting may differ slightly from the original file.');
downloadClientBuildHtml(cleanSnapshotHtml());
});
}
function clearLots(){
S.lots = []; S.prices = {};
lotFilterAccount = 'All'; lotFilterTerm = 'All'; lotFilterTicker = 'All';
rerender();
}
function resetDemo(){
try { localStorage.removeItem(STORE_KEY); } catch(e){}
S = defaultState();
lotFilterAccount = 'All'; lotFilterTerm = 'All'; lotFilterTicker = 'All';
resetPasswordsToDefault(true);
fillPanel();
rerender();
}
// Clears any saved client/advisor passwords -- including ones a stray autofill event may
// have written via the old input-based Sharing fields -- back to the hardcoded defaults
// (or this build's seed, if it has one). silent=true skips the extra fillPanel() call
// resetDemo() already does right after this.
function resetPasswordsToDefault(silent){
try { localStorage.removeItem(PW_KEY_CLIENT); } catch(e){}
try { localStorage.removeItem(PW_KEY_ADVISOR); } catch(e){}
CLIENT_PASSWORD = (CLIENT_SEED && CLIENT_SEED.clientPassword) || 'client';
ADVISOR_PASSWORD = (CLIENT_SEED && CLIENT_SEED.advisorPassword) || 'sully';
if(!silent) fillPanel();
}
// ============ CSV TAX LOT IMPORT ============
// Parses custodian exports entirely client-side. Auto-detects the header row
// (preamble lines are common), maps columns by fuzzy header match, and derives
// missing values: price from MV/qty, cost/share from total basis, basis from
// price minus gain. Handles $, commas, (negatives), and 3 date formats.
function parseCSVText(text){
var rows=[], row=[], cur='', inQ=false;
for(var i=0;i1 || row[0]!=='') rows.push(row); row=[]; }
else cur+=c;
}
}
if(cur!=='' || row.length){ row.push(cur); if(row.length>1 || row[0]!=='') rows.push(row); }
return rows;
}
function csvNum(v){
if(v==null) return null;
var s = String(v).trim().replace(/[$,\s%]/g,'');
if(!s) return null;
var neg = false;
var m = s.match(/^\((.*)\)$/); if(m){ neg = true; s = m[1]; }
if(s.charAt(0)==='-'){ neg = true; s = s.slice(1); }
var n = parseFloat(s);
if(isNaN(n)) return null;
return neg ? -n : n;
}
function csvDate(v){
if(v==null) return null;
var s = String(v).trim();
if(!s) return null;
function iso(y,mo,dd){
if(y<1950 || y>2040 || mo<1 || mo>12 || dd<1 || dd>31) return null;
return y + '-' + String(mo).padStart(2,'0') + '-' + String(dd).padStart(2,'0');
}
var m = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
if(m) return iso(+m[1], +m[2], +m[3]);
m = s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})$/);
if(m){
var y = +m[3]; if(y < 100) y += y > 40 ? 1900 : 2000;
return iso(y, +m[1], +m[2]);
}
var t = Date.parse(s);
if(!isNaN(t)){ var d = new Date(t); return iso(d.getFullYear(), d.getMonth()+1, d.getDate()); }
return null;
}
var CSV_FIELDS = [
{k:'symbol', label:'Symbol / Ticker', kw:['symbol','ticker','security id','security','instrument']},
{k:'qty', label:'Shares / Quantity', kw:['quantity','shares','qty','units','# of shares','share']},
{k:'date', label:'Date Acquired', kw:['acquired','acquisition','open date','opened','purchase date','purchased','trade date','lot date','date']},
{k:'costps', label:'Cost / Share', kw:['cost/share','cost per share','unit cost','per share cost','cost share','price paid','average cost']},
{k:'cost', label:'Cost Basis (total)', kw:['cost basis','total cost','adjusted cost','basis','cost']},
{k:'mv', label:'Market Value', kw:['market value','current value','mkt value','value']},
{k:'price', label:'Current Price', kw:['current price','last price','price']},
{k:'account', label:'Account', kw:['account','acct']},
{k:'term', label:'Term (LT/ST)', kw:['term','holding period','lt/st','long/short']},
{k:'gain', label:'Unrealized Gain', kw:['unrealized gain','gain/loss','unrealized g/l','gain']}
];
function detectCsv(rows){
var bestIdx = 0, bestScore = -1;
var limit = Math.min(rows.length, 12);
for(var r=0;r= 0; })) score++;
});
});
if(score > bestScore){ bestScore = score; bestIdx = r; }
}
var headers = rows[bestIdx].map(function(h){ return String(h).trim(); });
var map = {}; var used = {};
CSV_FIELDS.forEach(function(f){
for(var ki=0; ki= 0){ map[f.k] = c; used[c] = true; return; }
}
}
});
return { headerIdx: bestIdx, headers: headers, map: map, dataRows: rows.slice(bestIdx+1) };
}
function buildLots(det, priceLookup, defaultAccount){
var lots = [], skipped = 0, reasons = {};
var priceCand = {};
function skip(why){ skipped++; reasons[why] = (reasons[why]||0)+1; }
det.dataRows.forEach(function(row){
function cell(k){ return det.map[k] != null ? row[det.map[k]] : null; }
var sym = cell('symbol'); sym = sym ? String(sym).trim().toUpperCase().replace(/[^A-Z0-9.\-]/g,'') : '';
var qty = csvNum(cell('qty'));
if(!sym){ if(row.join('').trim()) skip('missing symbol'); return; }
if(sym === 'TOTAL' || sym === 'TOTALS') return;
if(qty == null || qty <= 0){ skip('missing/zero shares'); return; }
var costps = csvNum(cell('costps'));
var cost = csvNum(cell('cost'));
var mv = csvNum(cell('mv'));
var price = csvNum(cell('price'));
var gain = csvNum(cell('gain'));
var dateIso = csvDate(cell('date'));
if(price == null && mv != null && qty > 0) price = mv/qty;
var cb = costps != null ? costps : (cost != null && qty > 0 ? cost/qty : null);
if(cb == null && gain != null && price != null) cb = price - gain/qty;
if(cb == null){
var pl = price != null ? price : priceLookup(sym);
if(pl){ cb = pl * 0.4; skip('no cost basis, defaulted to 40% of price'); }
else { skip('no cost basis or price'); return; }
}
if(!dateIso){
var term = String(cell('term')||'').toUpperCase();
var ageDays = term.indexOf('S') === 0 ? 200 : 900;
dateIso = new Date(Date.now() - ageDays*DAY).toISOString().slice(0,10);
}
var acct = cell('account'); acct = acct ? String(acct).trim() : (defaultAccount || '100001');
if(price != null && price > 0){ (priceCand[sym] = priceCand[sym]||[]).push(price); }
lots.push({ a: acct, s: sym, q: qty, d: dateIso, cb: Math.max(cb, 0) });
});
var prices = {};
Object.keys(priceCand).forEach(function(sym){
var arr = priceCand[sym].sort(function(a,b){return a-b;});
prices[sym] = arr[Math.floor(arr.length/2)];
});
return { lots: lots, prices: prices, skipped: skipped, reasons: reasons };
}
var pendingCsv = null;
function parseCsvInput(){
var el = document.getElementById('csvMap');
var f = document.getElementById('csvFile').files[0];
var txt = document.getElementById('csvPaste').value;
if(f){
var r = new FileReader();
r.onload = function(){ handleCsvText(String(r.result)); };
r.onerror = function(){ el.innerHTML = '