Portfolio Construction: April 26, 2025
  • Home
  • Portfolio
  • Capital Allocation
  • Asset Class Allocation
  • Security Allocation
  • Data
  • Excel Models
  • IPS
Code
REPORT_DATE = "2025-04-26"
EFFICIENT_FRONTIER = `./plotly/efficient_frontier-2025-04-26.html`
Code
htl = require('htl')

// Button style definition
btnStyle = `
  margin-bottom: 10px;
  padding: 8px 16px;
  background: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
`

// Fullscreen handler
makeFullscreen = (iframeSrc) => {
  return htl.html`
    <div>
      <button style="${btnStyle}" onclick=${e => {
        const iframe = e.target.parentElement.querySelector('iframe');
        if (iframe) iframe.requestFullscreen();
      }}>Fullscreen</button>
      <div style="width: 100%; position: relative;">
        <iframe 
          class="frontier-iframe"
          src="${iframeSrc}"
          style="width: 100%; border:0; border-radius: 4px; overflow:hidden;"
        ></iframe>
      </div>
    </div>
  `
}
Code
utils = await import("./js/utils.js")
constants = await import("./js/constants.js")
assetColors = constants.assetColors

// Use dynamic import for utility functions
formatCurrency = utils.formatCurrency
formatNumber = utils.formatNumber
formatPercent = utils.formatPercent
createRangeInput = utils.createRangeInput
createPercentInput = utils.createPercentInput
createNumericInput = utils.createNumericInput
createCurrencyInput = utils.createCurrencyInput
createDateInput = utils.createDateInput
createAssetAllocationSlider = utils.createAssetAllocationSlider
createHoldingsTable = utils.createHoldingsTable
calculatePortfolioStats = utils.calculatePortfolioStats
calculateUtility = utils.calculateUtility
findOptimalEquityWeight = utils.findOptimalEquityWeight
calculateRiskBasedAllocation = utils.calculateRiskBasedAllocation
calculateRiskyPortfolioMetrics = utils.calculateRiskyPortfolioMetrics
categorizeValue = utils.categorizeValue
calculateData = utils.generatePortfolioData
processQuotesData = utils.processQuotesData
calculateAssetClassWeights = utils.calculateAssetClassWeights
calculateTickerWeights = utils.calculateTickerWeights
generateSamplePortfolioData = utils.generateSamplePortfolioData
calculatePortfolioReturns = utils.calculatePortfolioReturns
processWeights = utils.processWeights
convertToRowData = utils.convertToRowData
calculateTickerPerformance = utils.calculateTickerPerformance
calculatePortfolioTotals = utils.calculatePortfolioTotals
Code
dateRange = {
  if (!daily_quotes?.Date?.length) {
    console.warn("No valid date data in daily_quotes");
    return { startDate: new Date(), endDate: new Date() };
  }
  
  const dates = daily_quotes.Date.map(d => new Date(d));
  return { 
    startDate: new Date(Math.min(...dates)), 
    endDate: new Date(Math.max(...dates)) 
  };
};

// Process portfolio data
portfolioData = {
  // Use real data if available, otherwise generate sample data
  if (!daily_quotes?.Date?.length) {
    return generateSamplePortfolioData(equity_tickers, investment_amount);
  }
  return calculatePortfolioReturns(daily_quotes, equity_tickers, investment_amount);
};
Code
// Calculate dynamic asset allocations based on current inputs
dynamicAllocations = (() => { 
  // Use current parameter values
  const bondReturn = bond_ret; // Fixed value from default_assets
  const equityReturn = calc_equity_return || 0.237;
  const bondStd = bond_vol; // Fixed value from default_assets
  const equityStd = calc_equity_std || 0.161;
  const correlation = corr_eq_bd; // Fixed value from default_assets
  
  // Get current risk free rate from user input
  const riskFreeRate = rf_rate;
  
  // Clone the initial allocations structure but update with current values
  const allocations = JSON.parse(JSON.stringify(initial_allocations));
  
  // Recalculate allocations using current values
  allocations.allocations = [];
  for (let bondWeight = 0; bondWeight <= 100; bondWeight += 10) {
    const equityWeight = 100 - bondWeight;
    const w = [bondWeight/100, equityWeight/100];
    
    // Calculate expected return
    const er = w[0] * bondReturn + w[1] * equityReturn;
    
    // Calculate standard deviation (simplified portfolio variance formula)
    const variance = 
      Math.pow(w[0], 2) * Math.pow(bondStd, 2) + 
      Math.pow(w[1], 2) * Math.pow(equityStd, 2) + 
      2 * w[0] * w[1] * bondStd * equityStd * correlation;
    const stdDev = Math.sqrt(variance);
    
    // Calculate Sharpe ratio
    const sharpe = (er - riskFreeRate) / stdDev;
    
    allocations.allocations.push({
      bond_weight: bondWeight,
      equity_weight: equityWeight,
      expected_return: er,
      std_dev: stdDev,
      sharpe: sharpe
    });
  }
  
  // Recalculate minimum variance portfolio
  const minVar = allocations.allocations.reduce((prev, current) => 
    (current.std_dev < prev.std_dev) ? current : prev, allocations.allocations[0]);
  
  allocations.min_variance = {
    Return: minVar.expected_return,
    Risk: minVar.std_dev,
    Weights: [minVar.bond_weight/100, minVar.equity_weight/100],
    Sharpe: minVar.sharpe
  };
  
  // Recalculate max Sharpe portfolio
  const maxSharpe = allocations.allocations.reduce((prev, current) => 
    (current.sharpe > prev.sharpe) ? current : prev, allocations.allocations[0]);
  
  allocations.max_sharpe = {
    Return: maxSharpe.expected_return,
    Risk: maxSharpe.std_dev,
    Weights: [maxSharpe.bond_weight/100, maxSharpe.equity_weight/100],
    Sharpe: maxSharpe.sharpe
  };
  
  // Recalculate complete portfolio
  const optimalY = risk_aversion_weight; // Use current risk-based weight
  const rfWeight = 1 - optimalY;
  const bondWeight = optimalY * maxSharpe.bond_weight/100;
  const equityWeight = optimalY * maxSharpe.equity_weight/100;
  const erComplete = optimalY * maxSharpe.expected_return + rfWeight * riskFreeRate;
  const stdDevComplete = optimalY * maxSharpe.std_dev;
  
  allocations.complete_portfolio = {
    y: optimalY,
    rf_weight: rfWeight,
    bond_weight: bondWeight,
    equity_weight: equityWeight,
    er_complete: erComplete,
    std_dev_complete: stdDevComplete,
    sharpe: maxSharpe.sharpe
  };
  
  // Generate efficient frontier points (simplified)
  allocations.efficient_frontier = allocations.allocations.map(a => ({
    Return: a.expected_return,
    Risk: a.std_dev,
    Weights: [a.bond_weight/100, a.equity_weight/100]
  }));
  
  return allocations;
})()
Code
// Extract the data enrichment process to a separate cell
enrichedSecurityData = {
  const data = equity_tickers ?? [];
  if (data.length === 0) return [];

  /* ─── Totals by asset class ───────────────────────────── */
  let equityTotal = 0, bondTotal = 0, rfTotal = 0;
  data.forEach(d => {
    if (d.Type === "Equity")  equityTotal += d.Risky_Pf_Weight;
    else if (d.Type === "Bond") bondTotal += d.Risky_Pf_Weight;
    else if (d.Type === "Risk‑Free") rfTotal += d.Risky_Pf_Weight;
  });

  // Pre-calculate all weights for each security
  return data.slice().map(d => {
    // Select the appropriate asset class total
    let classTotal;
    if (d.Type === "Equity") classTotal = equityTotal;
    else if (d.Type === "Bond") classTotal = bondTotal;
    else if (d.Type === "Risk‑Free") classTotal = rfTotal;
    else classTotal = 0;
    
    // Calculate class weight (with special case for Risk-Free)
    let classWeight;
    if (d.Type === "Risk‑Free") {
      classWeight = 1.0; // Risk-Free assets have 100% weight in their class
    } else {
      classWeight = classTotal ? d.Risky_Pf_Weight / classTotal : 0;
    }
    
    // Handle Risk-Free asset class differently
    if (d.Type === "Risk‑Free") {
      return {
        ...d,
        classWeight, // Now correctly set to 1.0 (100%)
        riskyPfWeight: 0, // Risk-free assets are not part of the risky portfolio
        completeWeight: 1 - fixed_optimal_weight // The weight is the remainder after risky allocation
      };
    } else {
      // For Equity and Bond, use the regular calculation
      const riskyPfWeight = classWeight * (d.Type === "Equity" ? equity_weight : bond_weight);
      const completeWeight = riskyPfWeight * fixed_optimal_weight;
      
      return {
        ...d,
        classWeight,
        riskyPfWeight,
        completeWeight
      };
    }
  }).sort((a, b) => b.completeWeight - a.completeWeight); // Sort by complete weight
};

Portfolio Construction & Optimization

Date: April 26, 2025

Author: Renan Peres

View Source Code


This dashboard showcases an end-to-end process used for building and optimizing investment portfolios to support clients with their investment decisions. The methodology takes into account the risk-free rate, current market conditions, the client’s risk tolerance, and the diversification component from the risk–return profile of individual securities.

Note

This project is for educational use only.

It supports the accompanying Investment Policy Statement (IPS) and reflects the author’s opinions and analysis. No portion may be reproduced or distributed for commercial purposes without prior written consent.


Tip

Responsive sidebarsidebar inputs (located on the left side of the screen) — adjust the values and all the calculation, charts, and tables will be updated accordingly.


Portfolio Holdings Performance Analysis Future Projections
Overview of current investment holdings Historical returns and risk metrics Estimated growth scenarios and fee structure comparison
Benchmark Comparison Utility Weight Analysis Capital Allocation Line
Historical portfolio performance against benchmark Risk aversion and utility-based portfolio weights Optimal allocation between risky and risk-free assets
Asset Class Distribution Asset Class Efficient Frontier Security Distribution
Breakdown of portfolio by asset classes Risk-return profile of asset class allocations Individual security weightings within the portfolio
Equity Efficient Frontier Equity Covariances Bond Price Sensitivity
Risk-return profile of individual securities Correlation analysis between equity holdings Fixed income response to interest rate changes

Contents

Section Description
Portfolio Holdings, historical performance, expected return (with fee structure), and benchmark comparison
Capital Allocation Split between risk-free and risky assets based on the client’s risk tolerance (with utility functions)
Asset Class Allocation Weights across major asset classes with risk–return scenarios
Security Allocation Individual securities risk–return metrics, efficient frontier, covariances, and bond-price sensitivity to YTM
Data Raw datasets powering the dashboard
Excel Models Underlying portfolio-construction models
IPS Investment Policy Statement document

Security Selection

  • Equity Selection: 01_equity_portfolio_construction.ipynb
    Equity selection using fundamental analysis and Sharpe ratio maximization.

  • Bond Selection: 02_bond_portfolio_contruction.ipynb
    Fixed-income ETF screening for convexity > 1 and higher price sensitivity to changes in yield-to-maturity (YTM).

  • Benchmark Selection: 03_benchmark_selection.ipynb
    Regression analysis to identify an ETF (that invests in the same sectors) with the best fit to serve as the benchmark for the portfolio’s performance comparison.

  • Holdings
  • Historical Performance
  • Expected Return
  • Benchmark Comparison
Code
html`
  <div>
      <h5 class="table-title">Holdings Summary</h5>
      <table class="table table-sm mb-0">
        <tbody>
          <tr><th scope="row" style="width: 30%;">Allocation Date:</th><td>${d3.timeFormat("%B %d, %Y")(new Date(new Date(report_date).getTime() + 86400000))}</td></tr>
          <tr><th scope="row" style="width: 30%;">Investment Amount:</th><td>${formatCurrency(investment_amount)}</td></tr>
          <tr><th scope="row" style="width: 30%;">Time Horizon:</th><td>${time_horizon.toFixed(1)} yrs</td></tr>
          <tr><th scope="row" style="width: 30%;">Holdings:</th><td>${enrichedSecurityData ? enrichedSecurityData.length : 0}</td></tr>
        </tbody>
      </table>
      
      <div class="table-responsive">
        ${assetClassAllocationTable(3)}
      </div>
    </div>
  </div>`;
Code
viewof assetClassBarChart = {
  if (!enrichedSecurityData || enrichedSecurityData.length === 0) {
    return html`<div class="alert alert-warning">No asset class data available</div>`;
  }
  
  // Group by asset class and sum the complete weights
  const assetClassWeights = {
    "Equity": 0,
    "Bond": 0,
    "Risk-Free": 0  // Changed to standard hyphen to match constants.js
  };
  
  // Calculate total weight for each asset class
  enrichedSecurityData.forEach(d => {
    // Handle both dash styles (en-dash and standard hyphen)
    const type = d.Type === "Risk‑Free" ? "Risk-Free" : d.Type;
    if (type in assetClassWeights) {
      assetClassWeights[type] += d.completeWeight * 100;
    }
  });
  
  // Format data for plotting
  const barData = Object.entries(assetClassWeights)
    .map(([asset, weight]) => ({ asset, weight }))
    .filter(d => d.weight > 0)
    .sort((a, b) => b.weight - a.weight);
  
  const chart = Plot.plot({
    marginLeft: 70, marginRight: 40, marginTop: 0, marginBottom: 20,
    y: { 
      domain: barData.map(d => d.asset), 
      padding: 0.15, 
      tickFormat: d => d,
      label: null
    },
    x: { domain: [0, d3.max(barData, d => d.weight) * 1.1], axis: false, tickFormat: () => "" },
    marks: [
      Plot.barX(barData, { 
        y: "asset", 
        x: "weight", 
        fill: d => assetColors[d.asset], // Use global asset colors
        tip: true,
        title: d => `${d.asset}: ${d.weight.toFixed(1)}%` 
      }),
      Plot.text(barData, { 
        y: "asset", 
        x: "weight", 
        text: d => `${d.weight.toFixed(1)}%`,
        dx: 10, 
        textAnchor: "start", 
        fontWeight: "bold", 
        fontSize: 16 
      })
    ]
  });
  return html`<div><h5 class="table-title">Asset Class Distribution</h5>${chart}</div>`;
}
Code
html`
  <div>
    <h5 class="table-title">Security Allocation</h5>
    <table class="table table-sm mt-0 mb-0">${securityAllocationTable()}</table>
  </div>`
Code
viewof securityBarChart = {
  if (!enrichedSecurityData || enrichedSecurityData.length === 0) {
    return html`<div class="alert alert-warning">No security data available</div>`;
  }
  
  const displayData = enrichedSecurityData
    .map(d => ({ 
      ticker: d.Ticker, 
      name: d.Name || d.Ticker, 
      type: d.Type === "Risk‑Free" ? "Risk-Free" : d.Type, // Handle dash style differences
      weight: d.completeWeight * 100 
    }))
    .slice(0, 10);
  
  // Group securities by asset type
  const groupedByType = d3.group(displayData, d => d.type);
  
  // Create a color function that generates gradients for each type
  const getSecurityColor = (d) => {
    const baseColor = assetColors[d.type];
    const securities = groupedByType.get(d.type);
    
    // If there's only one security of this type, use the base color
    if (securities.length <= 1) return baseColor;
    
    // Otherwise create a gradient based on position in the group
    const index = securities.findIndex(s => s.ticker === d.ticker);
    const position = index / (securities.length - 1); // 0 to 1
    
    // Create a gradient from the base color to a lighter version
    return d3.interpolate(
      baseColor, 
      d3.color(baseColor).brighter(0.8)
    )(position);
  };

  const chart = Plot.plot({
    marginLeft: 70, marginRight: 40, marginTop: 0, marginBottom: 20,
    y: { 
      domain: displayData.map(d => d.ticker), 
      padding: 0.15,
      label: null 
    },
    x: { domain: [0, d3.max(displayData, d => d.weight) * 1.1], axis: false, tickFormat: () => "" },
    marks: [
      Plot.barX(displayData, { 
        y: "ticker", 
        x: "weight", 
        fill: getSecurityColor, // Use our custom gradient function
        tip: true, 
        title: d => `${d.ticker}: ${d.name}\n${d.weight.toFixed(1)}%` 
      }),
      Plot.text(displayData, { 
        y: "ticker", 
        x: "weight", 
        text: d => `${d.weight.toFixed(1)}%`,
        dx: 10, 
        textAnchor: "start", 
        fontWeight: "bold", 
        fontSize: 16 
      })
    ]
  });
  
  return html`<h5 class="table-title">Security Distribution</h5>${chart}</div>`;
}
Code
function buildPortfolioMetrics() {
  if (!portfolioData || portfolioData.length === 0)
    return { error: html`<div class="alert alert-warning">No portfolio data available</div>` };

  // Allow the line‑chart to override the dataset's end‑date
  const range = portfolioData.datasetDateRange ?? {};
  const startDate = range.startDate ?? new Date();
  const endDateStr = typeof selectedEndDate === "string" ? selectedEndDate : null;
  const endDate = endDateStr ? new Date(endDateStr) : (range.endDate ?? new Date());

  const yearsDiff = (endDate - startDate) / (1000 * 60 * 60 * 24 * 365);

  // Create ticker prices object with prices up to selectedEndDate
  const tickerPrices = {};
  
  // If we have raw daily_quotes data (which we should)
  if (daily_quotes?.Date && daily_quotes?.Ticker && daily_quotes?.Close) {
    const tickers = new Set();
    
    // Get all unique tickers
    for (let i = 0; i < daily_quotes.Ticker.length; i++) {
      tickers.add(daily_quotes.Ticker[i]);
    }
    
    // For each ticker, find first price and price at/before selected end date
    for (const ticker of tickers) {
      let firstDate = null;
      let firstPrice = null;
      let lastDate = null;
      let lastPrice = null;
      
      // Find first and last price for this ticker
      for (let i = 0; i < daily_quotes.Date.length; i++) {
        if (daily_quotes.Ticker[i] !== ticker) continue;
        
        const date = new Date(daily_quotes.Date[i]);
        const price = daily_quotes.Close[i];
        
        // Skip invalid prices
        if (price <= 0 || isNaN(price)) continue;
        
        // First price
        if (!firstDate || date < firstDate) {
          firstDate = date;
          firstPrice = price;
        }
        
        // Last price before or equal to endDate
        if (date <= endDate && (!lastDate || date >= lastDate)) {
          lastDate = date;
          lastPrice = price;
        }
      }
      
      // Store ticker data if we found valid prices
      if (firstPrice && lastPrice) {
        tickerPrices[ticker] = {
          firstPrice,
          lastPrice
        };
      }
    }
  } else {
    // Fallback code remains unchanged
    const fallbackPrices = processedQuotesData ?? portfolioData.tickerFirstLastPrices;
    if (!fallbackPrices || Object.keys(fallbackPrices).length === 0) {
      return { error: html`<div class="alert alert-warning">Incomplete portfolio data – missing price information</div>` };
    }
    
    for (const [ticker, priceData] of Object.entries(fallbackPrices)) {
      tickerPrices[ticker] = { 
        firstPrice: priceData.firstPrice,
        lastPrice: priceData.lastPrice
      };
    }
  }

  // Use enrichedSecurityData instead of recalculating weights
  const tickerData = {};
  let validCount = 0;

  // First check if we have enrichedSecurityData
  if (enrichedSecurityData && enrichedSecurityData.length > 0) {
    for (const security of enrichedSecurityData) {
      const ticker = security.Ticker;
      
      // Skip if we don't have price data for this ticker
      if (!tickerPrices[ticker]) continue;
      
      const prices = tickerPrices[ticker];
      if (!prices.firstPrice || !prices.lastPrice || prices.firstPrice <= 0 || prices.lastPrice <= 0) continue;
      
      validCount++;
      
      // Calculate values using weights from enrichedSecurityData
      const initial = investment_amount * security.completeWeight;
      const shares = initial / prices.firstPrice;
      
      // For Risk-Free assets, current value equals initial value regardless of date
      const current = security.Type === "Risk‑Free" ? initial : shares * prices.lastPrice;
      
      // Calculate returns
      const totRet = initial ? current / initial - 1 : 0;
      
      let annRet = 0;
      if (yearsDiff > 0 && totRet >= -1 && isFinite(totRet)) {
        annRet = Math.pow(1 + totRet, 1 / yearsDiff) - 1;
        if (!isFinite(annRet)) annRet = 0;
      }
      
      tickerData[ticker] = {
        type: security.Type,
        name: security.Name || ticker,
        sector: security.Sector || "N/A",
        assetClassWeight: security.classWeight,
        riskyPfWeight: security.riskyPfWeight,
        completeWeight: security.completeWeight,
        initialValue: initial,
        currentValue: current,
        totalReturn: totRet,
        annualizedReturn: annRet
      };
    }
  } else {
    // Fall back to original calculation if enrichedSecurityData is not available
    const classTotals = calculateAssetClassWeights(tickerPrices);
    
    for (const [ticker, price] of Object.entries(tickerPrices)) {
      if (!price.firstPrice || !price.lastPrice || price.firstPrice <= 0 || price.lastPrice <= 0) continue;

      const info = Array.isArray(equity_tickers) ? equity_tickers.find(t => t.Ticker === ticker) : {};
      const type = info?.Type ?? "Equity";
      validCount++;

      const w = calculateTickerWeights(
        ticker, type, price,
        classTotals, equity_weight, bond_weight,
        fixed_optimal_weight
      );

      const initial = investment_amount * w.completeWeight;
      const shares = initial / price.firstPrice;
      
      // For Risk-Free assets, current value equals initial value regardless of date
      const current = type === "Risk‑Free" ? initial : shares * price.lastPrice;
      
      const totRet = initial ? current / initial - 1 : 0;

      let annRet = 0;
      if (yearsDiff > 0 && totRet >= -1 && isFinite(totRet)) {
        annRet = Math.pow(1 + totRet, 1 / yearsDiff) - 1;
        if (!isFinite(annRet)) annRet = 0;
      }

      tickerData[ticker] = {
        type,
        name: info?.Name ?? ticker,
        sector: info?.Sector ?? "N/A",
        assetClassWeight: w.assetClassWeight,
        riskyPfWeight: w.riskyPfWeight,
        completeWeight: w.completeWeight,
        initialValue: initial,
        currentValue: current,
        totalReturn: totRet,
        annualizedReturn: annRet
      };
    }
  }

  // Rest of the function remains unchanged
  if (validCount === 0)
    return { error: html`<div class="alert alert-danger">No valid ticker data available for portfolio calculations</div>` };

  /* portfolio totals */
  const totals = Object.values(tickerData).reduce(
    (a, d) => ({ initial: a.initial + d.initialValue, current: a.current + d.currentValue }),
    { initial: 0, current: 0 }
  );

  const totalRet = totals.initial ? totals.current / totals.initial - 1 : 0;
  let irr = 0;
  if (yearsDiff > 0 && totalRet >= -1 && isFinite(totalRet)) {
    irr = Math.pow(1 + totalRet, 1 / yearsDiff) - 1;
    if (!isFinite(irr)) irr = 0;
  }

  return { startDate, endDate, yearsDiff, totals, totalRet, irr, tickerData };
}
Code
processedQuotesData = processQuotesData(daily_quotes, equity_tickers);
viewof selectedEndDate = {
  const input = html`<input type="hidden">`;
  input.value = (portfolioData?.datasetDateRange?.endDate ?? new Date()).toISOString();
  return input;
}
Code
function simplifiedAllocationTable(howMany = 3) {
  // Get the full table
  const fullTable = assetClassAllocationTable(howMany);
  
  // Create a new container for our filtered table
  const container = html`<div class="table-responsive" style="margin-top: 15px;">
    <table class="table table-sm">
      <thead>
        <tr>
          <th>Portfolio</th>
          <th>Weights</th>
          <th>Initial Value</th>
          <th>Current Value</th>
          <th>HPR</th>
          <th>IRR</th>
        </tr>
      </thead>
      <tbody></tbody>
    </table>
  </div>`;
  
  // Find rows in the original table
  const originalRows = fullTable.querySelectorAll('tr[data-idx]');
  const newTableBody = container.querySelector('tbody');
  
  // Get portfolio metrics for financial data
  const metrics = buildPortfolioMetrics();
  
  // Copy only the Portfolio and Weights columns and add financial metrics
  originalRows.forEach(originalRow => {
    const cells = originalRow.querySelectorAll('td');
    if (cells.length >= 2) {
      // Get portfolio type from the cell text content
      const portfolioTypeText = cells[0].textContent.trim();
      
      // Default financial metrics
      let initialValue = investment_amount;
      let currentValue = initialValue;
      let hpr = 0;
      let irr = 0;
      
      // If this row has portfolio data with weights, calculate financial metrics
      if (originalRow.portfolioData && !metrics.error && metrics.yearsDiff > 0) {
        const rowData = originalRow.portfolioData;
        
        // Extract the specific portfolio weights
        const riskyWeight = rowData.riskyWeight || 0;
        const rfWeight = 1 - riskyWeight;
        const equityWeight = rowData.equityWeight || 0;
        const bondWeight = 1 - equityWeight;
        
        // Calculate the complete portfolio weights
        const rfPercent = rfWeight;
        const equityPercent = riskyWeight * equityWeight;
        const bondPercent = riskyWeight * bondWeight;
        
        // Calculate asset-class specific returns from ticker data
        let rfReturn = 0;
        let bondReturn = 0;
        let equityReturn = 0;
        
        if (metrics.tickerData && Object.keys(metrics.tickerData).length > 0) {
          // Group ticker data by asset class
          const returns = {equity: [], bond: [], rf: []};
          const weights = {equity: [], bond: [], rf: []};
          
          // Extract returns by asset class
          for (const ticker in metrics.tickerData) {
            const data = metrics.tickerData[ticker];
            if (data.type === "Equity") {
              returns.equity.push(data.totalReturn);
              weights.equity.push(data.completeWeight);
            } else if (data.type === "Bond") {
              returns.bond.push(data.totalReturn);
              weights.bond.push(data.completeWeight);
            } else if (data.type === "Risk-Free" || data.type === "Risk‑Free") {
              returns.rf.push(data.totalReturn);
              weights.rf.push(data.completeWeight);
            }
          }
          
          // Calculate weighted returns for each asset class
          if (returns.equity.length > 0) {
            // Calculate a weighted average if we have weights
            if (weights.equity.some(w => w > 0)) {
              const totalWeight = weights.equity.reduce((sum, w) => sum + w, 0);
              equityReturn = returns.equity.reduce((sum, ret, i) => sum + ret * weights.equity[i] / totalWeight, 0);
            } else {
              // Simple average if no weights
              equityReturn = returns.equity.reduce((sum, ret) => sum + ret, 0) / returns.equity.length;
            }
          }
          
          if (returns.bond.length > 0) {
            if (weights.bond.some(w => w > 0)) {
              const totalWeight = weights.bond.reduce((sum, w) => sum + w, 0);
              bondReturn = returns.bond.reduce((sum, ret, i) => sum + ret * weights.bond[i] / totalWeight, 0);
            } else {
              bondReturn = returns.bond.reduce((sum, ret) => sum + ret, 0) / returns.bond.length;
            }
          }
          
          if (returns.rf.length > 0) {
            if (weights.rf.some(w => w > 0)) {
              const totalWeight = weights.rf.reduce((sum, w) => sum + w, 0);
              rfReturn = returns.rf.reduce((sum, ret, i) => sum + ret * weights.rf[i] / totalWeight, 0);
            } else {
              rfReturn = returns.rf.reduce((sum, ret) => sum + ret, 0) / returns.rf.length;
            }
          }
        }
        
        // If we couldn't get asset-specific returns, estimate them
        if (equityReturn === 0 && metrics.totalRet) {
          // Estimate equity returns as higher than the portfolio average
          equityReturn = metrics.totalRet * 1.5;
        }
        
        if (bondReturn === 0 && metrics.totalRet) {
          // Estimate bond returns as lower than the portfolio average
          bondReturn = metrics.totalRet * 0.7;
        }
        
        // Calculate the weighted portfolio return using this specific portfolio's asset allocation
        const portfolioReturn = rfPercent * rfReturn + equityPercent * equityReturn + bondPercent * bondReturn;
        
        // Calculate current value based on initial investment and weighted return
        initialValue = investment_amount;
        currentValue = initialValue * (1 + portfolioReturn);
        
        // Calculate HPR and IRR
        hpr = currentValue / initialValue - 1;
        
        // Calculate annualized return (IRR)
        if (hpr >= -1 && isFinite(hpr) && metrics.yearsDiff > 0) {
          irr = Math.pow(1 + hpr, 1 / metrics.yearsDiff) - 1;
          if (!isFinite(irr)) irr = 0;
        }
      }
      
      const newRow = html`<tr data-idx="${originalRow.getAttribute('data-idx')}" data-row-pos="${originalRow.getAttribute('data-row-pos')}">
        <td>${cells[0].innerHTML}</td>
        <td>${cells[1].innerHTML}</td>
        <td>${formatCurrency(initialValue)}</td>
        <td>${formatCurrency(currentValue)}</td>
        <td>${formatPercent(hpr)}</td>
        <td>${formatPercent(irr)}</td>
      </tr>`;
      
      // Copy the portfolioData for click handler
      newRow.portfolioData = originalRow.portfolioData;
      
      // Add the click handler
      newRow.addEventListener('click', function() {
        if (this.portfolioData) {
          // Update fixed_optimal_weight_input
          viewof fixed_optimal_weight_input.value = this.portfolioData.riskyWeight;
          viewof fixed_optimal_weight_input.dispatchEvent(new Event("input"));
          
          // Update equity_weight slider
          const equitySlider = viewof equity_weight;
          if (equitySlider) {
            const sliderInput = equitySlider.querySelector('input');
            if (sliderInput) {
              sliderInput.value = this.portfolioData.equityWeight;
              sliderInput.dispatchEvent(new Event("input"));
            }
            
            equitySlider.value = this.portfolioData.equityWeight;
            equitySlider.dispatchEvent(new Event("input"));
          }
          
          // Highlight the selected row
          container.querySelectorAll('tr').forEach(r => r.classList.remove('selected-row'));
          this.classList.add('selected-row');
          
          // Get portfolio type from the cell text content
          const cellText = this.querySelector('td').textContent.trim();
          let portfolioType = "Optimal Portfolio"; // Default
          
          if (cellText.includes("Minimum Variance")) {
            portfolioType = "Minimum Variance";
          } else if (cellText.includes("Maximum Variance")) {
            portfolioType = "Maximum Variance";
          }
          
          // Update the radio button state
          const portfolioTypeRadios = document.querySelectorAll('input[name="portfolioType"]');
          portfolioTypeRadios.forEach(radio => {
            radio.checked = (radio.value === portfolioType);
          });
        }
      });
      
      newTableBody.appendChild(newRow);
    }
  });
  
  return container;
}
Code
function portfolioPerformanceCard() {
  const m = buildPortfolioMetrics();
  if (m.error) return m.error;

  return html`
    <div>
      <table class="table table-sm mb-0">
        <tbody>
          <tr><th>Time Period:</th>
              <td>${d3.timeFormat("%b %d %Y")(m.startDate)} – ${d3.timeFormat("%b %d %Y")(m.endDate)}</td></tr>
          <tr><th>Initial Investment:</th><td>${formatCurrency(m.totals.initial)}</td></tr>
          <tr><th>Current Value:</th>     <td>${formatCurrency(m.totals.current)}</td></tr>
          <tr><th>Holding Period Return:</th><td>${formatPercent(m.totalRet)}</td></tr>
          <tr><th>IRR (Annualized):</th>  <td>${formatPercent(m.irr)}</td></tr>
        </tbody>
      </table>
    </div>`;
};

function createDateFilter() {
  // Get portfolio metrics to extract date range
  const metrics = buildPortfolioMetrics();
  if (metrics.error) return html`<div class="alert alert-warning">Cannot create date filter: ${metrics.error}</div>`;
  
  // Get date range
  const startDate = metrics.startDate;
  const endDate = metrics.endDate || new Date();
  const originalEndDate = portfolioData?.datasetDateRange?.endDate ?? new Date();
  const currentDate = new Date(viewof selectedEndDate.value);
  
  // Create date input component
  const dateInput = createDateInput({
    min: startDate,
    max: endDate,
    value: currentDate,
    label: "Portfolio Value as of:",
    id: "portfolio-date-filter"
  });
  
  // Add event listener to update selectedEndDate when the date input changes
  dateInput.addEventListener("input", () => {
    const input = viewof selectedEndDate;
    input.value = dateInput.value.toISOString();
    input.dispatchEvent(new Event("input", { bubbles: true }));
  });
  
  // Create reset button
  const resetButton = document.createElement("button");
  resetButton.textContent = "Reset";
  resetButton.className = "btn btn-sm btn-outline-secondary";
  resetButton.style.marginLeft = "10px";
  resetButton.title = "Reset to the portfolio's final date";
  
  // Add event listener to reset the date
  resetButton.addEventListener("click", () => {
    // Update the date input
    dateInput.value = originalEndDate;
    
    // Update selectedEndDate
    const input = viewof selectedEndDate;
    input.value = originalEndDate.toISOString();
    input.dispatchEvent(new Event("input", { bubbles: true }));
  });
  
  // Create the container with flex display to align items
  const container = document.createElement("div");
  container.className = "date-filter d-flex align-items-center";
  container.appendChild(dateInput);
  container.appendChild(resetButton);
  
  return container;
};
Code
{
  const m = buildPortfolioMetrics();
  if (m.error) return m.error;
  
  // HTML with JS interpolation
  return html`
    <div>
      <h5 class="table-title">Portfolio Performance (${m.yearsDiff.toFixed(1)} yrs)</h5>
      <table class="table table-sm mt-0 mb-0">
        ${createDateFilter()}
        ${portfolioPerformanceCard()}
        ${simplifiedAllocationTable(3)}
      </table>
    </div>
  `;
}
Code
viewof portfolioValueChart = {
  /* ─── Input Validation ───────────────────────────────── */
  if (!daily_quotes?.Ticker || !daily_quotes?.Date || !daily_quotes?.Close)
    return html`<div class="alert alert-warning">No price time‑series data available for charting</div>`;

  const tickerPrices = processedQuotesData ?? {};
  if (Object.keys(tickerPrices).length === 0)
    return html`<div class="alert alert-warning">No ticker price information available</div>`;

  /* ─── Prepare Asset Weights & Types ────────────────── */
  const tickerWeights = {};
  const tickerTypes = {};
  
  // Use enriched security data if available, otherwise calculate weights
  if (enrichedSecurityData?.length > 0) {
    enrichedSecurityData.forEach(security => {
      const ticker = security.Ticker;
      if (tickerPrices[ticker]) {
        tickerWeights[ticker] = { weight: security.completeWeight, type: security.Type };
        tickerTypes[ticker] = security.Type;
      }
    });
  } else {
    const assetClassTotals = calculateAssetClassWeights(tickerPrices);
    
    for (const [ticker, priceData] of Object.entries(tickerPrices)) {
      if (!priceData.firstPrice || !priceData.lastPrice || priceData.firstPrice <= 0 || priceData.lastPrice <= 0) continue;

      const info = Array.isArray(equity_tickers) ? equity_tickers.find(t => t.Ticker === ticker) : {};
      const type = info?.Type ?? priceData.type ?? "Equity";

      tickerWeights[ticker] = {
        weight: calculateTickerWeights(ticker, type, priceData,
                                    assetClassTotals, equity_weight,
                                    bond_weight, fixed_optimal_weight).completeWeight,
        type
      };
      tickerTypes[ticker] = type;
    }
  }

  /* ─── Create Price Data Matrix by Date ─────────────── */
  const dateTickerPrices = {};
  for (let i = 0; i < daily_quotes.Date.length; i++) {
    const date = new Date(daily_quotes.Date[i]).toISOString().split("T")[0];
    const ticker = daily_quotes.Ticker[i];
    const close = daily_quotes.Close[i];
    if (!dateTickerPrices[date]) dateTickerPrices[date] = {};
    dateTickerPrices[date][ticker] = close;
  }
  const sortedDates = Object.keys(dateTickerPrices).sort();

  /* ─── Find Starting Date with Complete Data ─────────── */
  const earliest = sortedDates.find(date =>
    Object.keys(tickerWeights).every(t => Number.isFinite(dateTickerPrices[date]?.[t]))
  );
  if (!earliest)
    return html`<div class="alert alert-warning">Cannot find a date where all tickers have valid price data</div>`;

  /* ─── Calculate Initial Investments & Shares ───────── */
  const tickerShares = {};
  const tickerInitialValues = {};
  
  for (const ticker of Object.keys(tickerWeights)) {
    const initPx = dateTickerPrices[earliest][ticker];
    const initInv = investment_amount * tickerWeights[ticker].weight;
    tickerInitialValues[ticker] = initInv;
    
    // Calculate shares only for non-risk-free assets
    if (tickerTypes[ticker] !== "Risk‑Free") {
      tickerShares[ticker] = initInv / initPx;
    }
  }

  /* ─── Generate Portfolio Time Series ───────────────── */
  const portfolioTS = sortedDates.flatMap(date => {
    const prices = dateTickerPrices[date];
    // We only need valid prices for non-risk-free assets
    const validPrices = Object.keys(tickerWeights)
      .filter(t => tickerTypes[t] !== "Risk‑Free")
      .every(t => Number.isFinite(prices?.[t]));
      
    if (!validPrices) return [];
    
    // Calculate portfolio value
    let pv = 0;
    for (const ticker of Object.keys(tickerWeights)) {
      // Risk-Free assets maintain initial value, others follow market prices
      pv += tickerTypes[ticker] === "Risk‑Free" 
        ? tickerInitialValues[ticker]
        : prices[ticker] * tickerShares[ticker];
    }
    
    return [{ Date: new Date(date), Close: pv }];
  });

  if (portfolioTS.length === 0)
    return html`<div class="alert alert-warning">Not enough data points to generate chart</div>`;

  /* ─── Calculate Performance Metrics ───────────────── */
  const initVal = portfolioTS[0].Close;
  const currVal = portfolioTS.at(-1).Close;
  const totalRet = initVal ? currVal / initVal - 1 : 0;
  const startDate = portfolioTS[0].Date;
  const endDate = portfolioTS.at(-1).Date;
  const yearsDiff = (endDate - startDate) / (1000 * 60 * 60 * 24 * 365);
  const perfColor = totalRet >= 0 ? "#28a745" : "#dc3545";

  /* ─── Generate Plot ─────────────────────────────────── */
  function createPlot() {
    return Plot.plot({
      marginLeft: 80, marginRight: 20, marginBottom: 80,
      style: { fontSize: "14px", background: "transparent", overflow: "visible" },
      x: {
        type: "time",
        label: "Date:",
        domain: [startDate, endDate],
        tickFormat: d => {
          const year = d.getFullYear();
          const month = d.getMonth();
          // Show full date at beginning of years, only months otherwise
          return month === 0 ? d3.timeFormat("%Y")(d) : d3.timeFormat("%b")(d);
        }
      },
      y: {
        grid: true,
        label: "Portfolio Value ($):",
        labelOffset: 45,
        domain: [0, Math.max(d3.max(portfolioTS, d => d.Close) * 1.05, initVal * 1.05)],
        tickFormat: d => `$${d3.format(",")(d.toFixed(0))}`
      },
      marks: [
        // Initial investment reference line
        Plot.ruleY([initVal], { stroke: "#0b3040", strokeWidth: 2.5, strokeDasharray: "4 4" }),
        Plot.text([{ x: startDate, y: initVal }], {
          text: `Initial Investment: ${formatCurrency(initVal)}`,
          dy: -8, fontSize: 12, fontWeight: "bold", textAnchor: "start"
        }),
        
        // Portfolio value area and line
        Plot.areaY(portfolioTS, { 
          x: "Date", 
          y: "Close", 
          fill: perfColor, 
          fillOpacity: 0.1, 
          curve: "natural" 
        }),
        Plot.lineY(portfolioTS, {
          x: "Date", 
          y: "Close", 
          stroke: perfColor, 
          strokeWidth: 3, 
          curve: "natural",
          tip: { 
            format: { 
              x: d => d3.timeFormat("%b %d, %Y")(d),
              y: v => formatCurrency(v),
              "Gain:": v => `${((v / initVal - 1) * 100).toFixed(2)}%`
            }
          },
          channels: {
            "Gain:": d => d.Close
          }
        }),
        
        // Current value annotations
        Plot.text([portfolioTS.at(-1)], {
          x: "Date", y: "Close",
          text: `Current Value: ${formatCurrency(currVal)}`,
          dx: 5, dy: -15, fontSize: 14, fontWeight: "bold"
        }),
        Plot.text([portfolioTS.at(-1)], {
          x: "Date", y: "Close",
          text: `${totalRet >= 0 ? "+" : ""}${(totalRet * 100).toFixed(2)}%`,
          dx: 5, dy: 15, fontSize: 14, fontWeight: "bold", fill: perfColor
        }),
        
        // Hover interaction elements
        Plot.ruleX(portfolioTS, Plot.pointerX({
          x: "Date", 
          stroke: perfColor, 
          strokeWidth: 1.5, 
          strokeDasharray: "4 4"
        })),
        Plot.dot(portfolioTS, Plot.pointerX({
          x: "Date", 
          y: "Close", 
          fill: perfColor, 
          stroke: "white", 
          strokeWidth: 2, 
          r: 5
        }))
      ]
    });
  }

  /* ─── Create Plot Container ────────────────────────── */
  const plotContainer = html`<div class="plot-container"></div>`;
  let plot = createPlot();
  plotContainer.appendChild(plot);

  /* ─── Create Interactive Container ──────────────────── */
  const container = html`<div style="position: relative;">
    <div class="chart-loading-overlay" style="display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; 
        background: rgba(255,255,255,0.7); z-index: 10; align-items: center; justify-content: center;">
      <div style="text-align: center;">
        <div class="spinner-border text-primary" role="status"></div>
        <div style="margin-top: 10px; font-weight: bold;">Updating metrics...</div>
      </div>
    </div>
    <div>
      <h5 class="chart-title">Portfolio Value Over Time (${buildPortfolioMetrics().yearsDiff.toFixed(1)} yrs)</h5>
    ${plotContainer}
    </div>
    <div class="hover-tooltip" style="position: absolute; display: none; background: white; 
        border: 1px solid #ddd; border-radius: 3px; padding: 8px; pointer-events: none;
        box-shadow: 0 2px 4px rgba(0,0,0,0.1); z-index: 20; font-size: 14px;"></div>
    <div class="hover-dot" style="position: absolute; width: 12px; height: 12px; border-radius: 50%; 
        background-color: ${perfColor}; border: 2px solid white; display: none; transform: translate(-50%, -50%); 
        pointer-events: none; z-index: 15;"></div>
  </div>`;
  
  const loadingOverlay = container.querySelector(".chart-loading-overlay");
  const hoverTooltip = container.querySelector(".hover-tooltip");
  const hoverDot = container.querySelector(".hover-dot");
  
  /* ─── Function to hide tooltip elements ────────────── */
  function hideTooltipElements() {
    if (hoverDot) hoverDot.style.display = "none";
    if (hoverTooltip) hoverTooltip.style.display = "none";
    
    // Clear any Plot.js tooltips by removing them from the DOM
    const plotTooltips = document.querySelectorAll('.plot-tooltip');
    plotTooltips.forEach(tooltip => tooltip.remove());
  }
  
  /* ─── Add Hover Interaction ────────────────────────── */
  function setupHoverInteraction() {
    plot.addEventListener("pointermove", (event) => {
      // Skip if currently processing a click
      if (window.isClickProcessing) return;
      
      const { left, top } = plot.getBoundingClientRect();
      const px = event.clientX - left;
      const date = plot.scale("x").invert(px);
      
      if (!date || isNaN(date)) {
        hideTooltipElements();
        return;
      }
      
      // Find the closest data point
      const closest = portfolioTS.reduce((best, d) => 
        Math.abs(d.Date - date) < Math.abs(best.Date - date) ? d : best
      );
      
      // Position the dot at the data point
      const dotX = plot.scale("x")(closest.Date);
      const dotY = plot.scale("y")(closest.Close);
      
      if (isNaN(dotX) || isNaN(dotY)) {
        hideTooltipElements();
        return;
      }
      
      // Update hover elements
      hoverDot.style.left = `${dotX}px`;
      hoverDot.style.top = `${dotY}px`;
      hoverDot.style.display = "block";
      
      // Show tooltip with date and value
      hoverTooltip.innerHTML = `
        <div><strong>${d3.timeFormat("%b %d, %Y")(closest.Date)}</strong></div>
        <div>Value: ${formatCurrency(closest.Close)}</div>
      `;
      hoverTooltip.style.display = "block";
      
      // Position tooltip with boundary checking
      const tooltipX = Math.min(event.clientX - left + 10, plot.clientWidth - hoverTooltip.offsetWidth - 5);
      const tooltipY = Math.max(event.clientY - top - hoverTooltip.offsetHeight - 10, 5);
      hoverTooltip.style.transform = `translate(${tooltipX}px, ${tooltipY}px)`;
    });
    
    // Hide elements when pointer leaves chart
    plot.addEventListener("pointerleave", () => {
      // Only hide if not processing a click
      if (!window.isClickProcessing) {
        hideTooltipElements();
      }
    });
  }
  
  /* ─── Add Click Interaction ─────────────────────────── */
  function setupClickInteraction() {
    plot.addEventListener("click", ev => {
      // Use a global flag to prevent tooltip flickering across different charts
      window.isClickProcessing = true;
      
      const { left } = plot.getBoundingClientRect();
      const px = ev.clientX - left;
      const date = plot.scale("x").invert(px);
      if (!date || isNaN(date)) {
        window.isClickProcessing = false;
        return;
      }
      
      // Find nearest data point
      const closest = portfolioTS.reduce((best, d) => 
        Math.abs(d.Date - date) < Math.abs(best.Date - date) ? d : best
      );
      
      // Hide tooltip and hover dot immediately
      hideTooltipElements();
      
      // Show loading overlay
      loadingOverlay.style.display = "flex";
      
      // Update selected date and trigger event
      const input = viewof selectedEndDate;
      input.value = closest.Date.toISOString();
      
      // Update UI
      requestAnimationFrame(() => {
        input.dispatchEvent(new CustomEvent("input", { bubbles: true }));
        
        // Completely regenerate the plot to eliminate any lingering tooltips
        setTimeout(() => {
          // Hide loading overlay
          loadingOverlay.style.display = "none";
          
          // Completely replace the plot element
          plotContainer.innerHTML = '';
          plot = createPlot();
          plotContainer.appendChild(plot);
          
          // Re-setup event listeners
          setupHoverInteraction();
          setupClickInteraction();
          
          // Ensure all tooltips are gone
          hideTooltipElements();
          
          // Reset the processing flag
          window.isClickProcessing = false;
        }, 300);
      });
    });
  }

  // Initial setup of event listeners
  setupHoverInteraction();
  setupClickInteraction();

  return container;
}
Code
function securityPerformanceTable() {
  const m = buildPortfolioMetrics();
  if (m.error) return m.error;

  return html`
    <div class="table-responsive">
      <h5 class="table-title">Security Performance (${m.yearsDiff.toFixed(1)} yrs)</h5>
      <table class="table table-sm table-hover">
        <thead>
          <tr>
            <th>Ticker</th>
            <th>Name</th>
            <th>Weight</th>
            <th>Initial&nbsp;Value</th>
            <th>Current&nbsp;Value</th>
            <th>HPR</th>
            <th>IRR</th>
          </tr>
        </thead>
        <tbody>
          ${Object.entries(m.tickerData)
            .sort(([_1,d1],[_2,d2]) => d2.completeWeight - d1.completeWeight)
            .map(([t,d]) => html`
              <tr>
                <td><strong>${t}</strong></td>
                <td>${d.name}</td>
                <td>${formatPercent(d.completeWeight)}</td>
                <td>${formatCurrency(d.initialValue)}</td>
                <td>${formatCurrency(d.currentValue)}</td>
                <td>${formatPercent(d.totalReturn)}</td>
                <td>${formatPercent(d.annualizedReturn)}</td>
              </tr>`)}
        </tbody>
      </table>
    </div>`;
}
Code
securityPerformanceTable()
Code
viewof tickerPerformanceChart = {
  /* ─── Input Validation ───────────────────────────────── */
  if (!daily_quotes?.Ticker || !daily_quotes?.Date || !daily_quotes?.Close)
    return html`<div class="alert alert-warning">No price time‑series data available for charting</div>`;

  if (!enrichedSecurityData?.length)
    return html`<div class="alert alert-warning">No security data available for charting</div>`;

  /* ─── Prepare Asset Data ────────────────────────────── */
  // Only include tickers that exist in our portfolio
  const portfolioTickers = new Set(enrichedSecurityData.map(d => d.Ticker));
  
  // Get end date from selected date or default to portfolio end date
  const endDateStr = typeof selectedEndDate === "string" ? selectedEndDate : null;
  const endDate = endDateStr ? new Date(endDateStr) : (portfolioData?.datasetDateRange?.endDate ?? new Date());
  
  /* ─── Create Price Matrix by Date ─────────────────── */
  const tickerPricesByDate = {};
  const tickerTypes = {};
  const tickerInfo = {};
  
  // Map ticker names to types and display names
  enrichedSecurityData.forEach(security => {
    const ticker = security.Ticker;
    tickerTypes[ticker] = security.Type;
    tickerInfo[ticker] = {
      name: security.Name || ticker,
      type: security.Type,
      sector: security.Sector || "N/A",
      weight: security.completeWeight
    };
  });
  
  // Group price data by date
  for (let i = 0; i < daily_quotes.Date.length; i++) {
    const ticker = daily_quotes.Ticker[i];
    
    // Skip if ticker is not in our portfolio
    if (!portfolioTickers.has(ticker)) continue;
    
    const date = new Date(daily_quotes.Date[i]);
    // Skip if date is after endDate
    if (date > endDate) continue;
    
    const dateStr = date.toISOString().split("T")[0];
    const close = daily_quotes.Close[i];
    
    if (!tickerPricesByDate[dateStr]) tickerPricesByDate[dateStr] = {};
    tickerPricesByDate[dateStr][ticker] = close;
  }
  
  /* ─── Find Starting Date with Complete Data ─────────── */
  const sortedDates = Object.keys(tickerPricesByDate).sort();
  // We'll use the same start date as the portfolio chart for consistency
  const portfolioMetrics = buildPortfolioMetrics();
  const startDate = portfolioMetrics?.startDate ?? new Date(sortedDates[0]);
  
  // Filter out dates before start date
  const validDates = sortedDates.filter(dateStr => new Date(dateStr) >= startDate);
  
  if (validDates.length === 0) {
    return html`<div class="alert alert-warning">Not enough data available within the selected date range</div>`;
  }
  
  const startDateStr = validDates[0];
  const firstPrices = tickerPricesByDate[startDateStr];
  
  // Skip risk-free assets as they don't have variable prices
  const relevantTickers = [...portfolioTickers].filter(t => tickerTypes[t] !== "Risk‑Free");
  
  /* ─── Create Normalized Series ─────────────────────── */
  // Generate time series data for each ticker
  const series = [];
  const colors = d3.scaleOrdinal(d3.schemeCategory10);
  
  // Create a normalized series for each ticker
  for (const ticker of relevantTickers) {
    // Skip tickers with no starting price
    if (!firstPrices[ticker] || firstPrices[ticker] <= 0) continue;
    
    const basePrice = firstPrices[ticker];
    const values = [];
    
    for (const date of validDates) {
      const price = tickerPricesByDate[date][ticker];
      if (!Number.isFinite(price) || price <= 0) continue; // Skip invalid prices
      
      values.push({
        date: new Date(date),
        value: price / basePrice, // Normalize to starting price
        price: price
      });
    }
    
    // Only add series with enough data points
    if (values.length > 5) {
      series.push({
        ticker,
        name: tickerInfo[ticker].name,
        type: tickerInfo[ticker].type,
        sector: tickerInfo[ticker].sector,
        weight: tickerInfo[ticker].weight,
        values,
        color: colors(ticker)
      });
    }
  }
  
  // Sort series by return (highest to lowest)
  series.sort((a, b) => {
    const aReturn = a.values[a.values.length - 1].value;
    const bReturn = b.values[b.values.length - 1].value;
    return bReturn - aReturn; // Descending order
  });
  
  // Early exit if we don't have enough data
  if (series.length === 0) {
    return html`<div class="alert alert-warning">Not enough price data available for tickers in portfolio</div>`;
  }
  
  /* ─── Create Selection State ─────────────────────── */
  // Initialize state to track which tickers are visible
  const state = {
    visibleTickers: new Set(series.map(s => s.ticker)), // Start with all visible
    showingAll: true
  };
  
  /* ─── Calculate Chart Metrics ───────────────────── */
  const yearsDiff = (endDate - startDate) / (1000 * 60 * 60 * 24 * 365);
  const allValues = series.flatMap(s => s.values);
  const maxValue = d3.max(allValues, d => d.value);
  const minValue = d3.min(allValues, d => d.value);
  
  /* ─── Function to Generate Plot ───────────────────── */
  function generatePlot() {
    // Filter to only show visible series
    const visibleSeries = series.filter(s => state.visibleTickers.has(s.ticker));
    const visibleValues = visibleSeries.flatMap(s => s.values);
    
    // If nothing is visible, use all data for proper scaling
    const valuesToScale = visibleValues.length > 0 ? visibleValues : allValues;
    
    // Get min and max values for the visible data
    const maxVisibleValue = d3.max(valuesToScale, d => d.value);
    const minVisibleValue = d3.min(valuesToScale, d => d.value);
    
    // Calculate deviations from the 1.0 baseline
    const deviationAbove = maxVisibleValue - 1.0;
    const deviationBelow = 1.0 - minVisibleValue;
    
    // Use the larger deviation to determine axis range, ensuring 1.0 is centered
    const maxDeviation = Math.max(deviationAbove, deviationBelow);
    // Add 10% padding to the range for better visualization
    const paddedDeviation = maxDeviation * 1.1;
    
    return Plot.plot({
      marginLeft: 80, 
      marginRight: 160, // Increased for legend
      marginTop: 30,
      marginBottom: 100,
      style: { fontSize: "14px", background: "transparent", overflow: "visible" },
      x: {
        type: "time",
        label: "Date:",
        domain: [startDate, endDate],
        tickFormat: d => {
          const year = d.getFullYear();
          const month = d.getMonth();
          return month === 0 ? d3.timeFormat("%Y")(d) : d3.timeFormat("%b")(d);
        }
      },
      y: {
        grid: true,
        label: "Return (Start = 1.0x):",
        labelOffset: 45,
        // Set domain to center the 1.0 mark
        domain: [
          Math.max(0, 1.0 - paddedDeviation), // Ensure we don't go below 0
          1.0 + paddedDeviation
        ],
        tickFormat: d => d.toFixed(2) + "×"
      },
      marks: [
        // Reference line at 1.0 (starting value)
        Plot.ruleY([1], { stroke: "#0b3040", strokeWidth: 2, strokeDasharray: "4 4" }),
        Plot.text([{x: startDate, y: 1}], {
          text: "Starting Value (1.0×)",
          dy: -8, dx: 10, fontSize: 12, textAnchor: "start"
        }),
        
        // Lines for each ticker (only visible ones)
        ...visibleSeries.map(s => 
          Plot.lineY(s.values, {
            x: "date", 
            y: "value", 
            stroke: s.color, 
            strokeWidth: 2, 
            curve: "natural",
            tip: {
              format: {
                x: d => d3.timeFormat("%b %d, %Y")(d),
                "Ticker:": () => s.ticker,
                y: v => v.toFixed(2) + "×",
                "Return:": v => `${((v - 1) * 100).toFixed(2)}%`,
                "Name:": () => s.name,
                "Type:": () => s.type,
                "Price:": (_, i, data) => `$${data[i].price.toFixed(2)}`,
                "Weight:": () => (s.weight * 100).toFixed(2) + "%"
            }
          },
          channels: {
            "Ticker:": () => s.name
          }
        })
        ),

        // Dots at the end of each series for current values
        ...visibleSeries.map(s => {
          const lastPoint = s.values[s.values.length - 1];
          return Plot.dot([lastPoint], {
            x: "date", 
            y: "value", 
            fill: s.color, 
            stroke: "white", 
            strokeWidth: 1.5, 
            r: 4
          });
        }),
        
        // Labels at the end of each line
        ...visibleSeries.map(s => {
          const lastPoint = s.values[s.values.length - 1];
          const return_pct = ((lastPoint.value - 1) * 100).toFixed(1);
          const sign = lastPoint.value >= 1 ? "+" : "";
          return Plot.text([lastPoint], {
            x: "date", 
            y: "value",
            text: `${s.ticker} (${sign}${return_pct}%)`,
            dx: 10, 
            dy: 0, 
            fontSize: 12, 
            fontWeight: "bold", 
            fill: s.color,
            stroke: "white",
            strokeWidth: 3,
            paintOrder: "stroke"
          });
        }),
        
        // Hover interaction elements (only for visible series)
        Plot.ruleX(visibleValues, Plot.pointerX({
          x: "date", 
          stroke: "#666", 
          strokeWidth: 1, 
          strokeDasharray: "4 4"
        }))
      ]
    });
  }
  
  /* ─── Create Plot Container ─────────────────────── */
  const plotContainer = html`<div class="plot-container"></div>`;
  const plot = generatePlot();
  plotContainer.appendChild(plot);
  
  /* ─── Create Legend with Interactive Elements ───── */
  function createLegendItem(s) {
    const lastValue = s.values[s.values.length - 1].value;
    const returnPct = ((lastValue - 1) * 100).toFixed(2);
    const returnColor = lastValue >= 1 ? "#28a745" : "#dc3545";
    
    const item = html`<div class="legend-item ${state.visibleTickers.has(s.ticker) ? 'active' : 'inactive'}" 
                     style="display: flex; align-items: center; margin-bottom: 4px; cursor: pointer;
                           ${!state.visibleTickers.has(s.ticker) ? 'opacity: 0.5;' : ''}">
      <div style="width: 12px; height: 12px; background: ${s.color}; margin-right: 6px;"></div>
      <div style="flex-grow: 1;">${s.ticker} (${s.weight*100 < 10 ? (s.weight*100).toFixed(1) : Math.round(s.weight*100)}%)</div>
      <div style="width: 60px; text-align: right; color: ${returnColor};">${returnPct}%</div>
    </div>`;
    
    // Add click handler to toggle visibility
    item.addEventListener('click', () => {
      if (state.showingAll && state.visibleTickers.size === series.length) {
        // If showing all, switch to showing only this one
        state.visibleTickers.clear();
        state.visibleTickers.add(s.ticker);
        state.showingAll = false;
      } else if (state.visibleTickers.size === 1 && state.visibleTickers.has(s.ticker)) {
        // If this is the only one visible, show all
        series.forEach(s => state.visibleTickers.add(s.ticker));
        state.showingAll = true;
      } else {
        // Otherwise toggle this ticker
        if (state.visibleTickers.has(s.ticker)) {
          state.visibleTickers.delete(s.ticker);
        } else {
          state.visibleTickers.add(s.ticker);
          // Check if all are now visible
          if (state.visibleTickers.size === series.length) {
            state.showingAll = true;
          }
        }
      }
      
      // Update legend items
      legendContainer.querySelectorAll('.legend-item').forEach((item, idx) => {
        const ticker = series[idx].ticker;
        if (state.visibleTickers.has(ticker)) {
          item.classList.add('active');
          item.classList.remove('inactive');
          item.style.opacity = '1';
        } else {
          item.classList.remove('active');
          item.classList.add('inactive');
          item.style.opacity = '0.5';
        }
      });
      
      // Update show/hide all button
      updateShowAllButton();
      
      // Regenerate the plot
      plotContainer.innerHTML = '';
      plotContainer.appendChild(generatePlot());
    });
    
    return item;
  }
  
  // Create legend items
  const legendItems = series.map(s => createLegendItem(s));
  
  // Create Show All / Hide All button
  const showAllButton = html`<button class="btn btn-sm btn-outline-secondary" 
                            style="width: 100%; margin-top: 10px;">Hide All</button>`;
  
  function updateShowAllButton() {
    if (state.visibleTickers.size === series.length) {
      showAllButton.textContent = "Hide All";
      showAllButton.classList.add('btn-outline-secondary');
      showAllButton.classList.remove('btn-primary');
    } else if (state.visibleTickers.size === 0) {
      showAllButton.textContent = "Show All";
      showAllButton.classList.remove('btn-outline-secondary');
      showAllButton.classList.add('btn-primary');
    } else {
      showAllButton.textContent = "Show All";
      showAllButton.classList.remove('btn-outline-secondary');
      showAllButton.classList.add('btn-primary');
    }
  }
  
  // Add click handler to show/hide all button
  showAllButton.addEventListener('click', () => {
    if (state.visibleTickers.size === series.length) {
      // Hide all
      state.visibleTickers.clear();
      state.showingAll = false;
    } else {
      // Show all
      series.forEach(s => state.visibleTickers.add(s.ticker));
      state.showingAll = true;
    }
    
    // Update legend items
    legendContainer.querySelectorAll('.legend-item').forEach((item, idx) => {
      const ticker = series[idx].ticker;
      if (state.visibleTickers.has(ticker)) {
        item.classList.add('active');
        item.classList.remove('inactive');
        item.style.opacity = '1';
      } else {
        item.classList.remove('active');
        item.classList.add('inactive');
        item.style.opacity = '0.5';
      }
    });
    
    // Update button
    updateShowAllButton();
    
    // Regenerate the plot
    plotContainer.innerHTML = '';
    plotContainer.appendChild(generatePlot());
  });
  
  // Create legend container
  const legendContainer = html`<div class="ticker-legend" style="position: absolute; top: 10px; right: 10px; 
                               background: rgba(255,255,255,0.9); border: 1px solid #ddd; border-radius: 4px; 
                               padding: 8px; max-height: 85%; overflow-y: auto; font-size: 12px;">
    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
      <div style="font-weight: bold; font-size: 14px;">Tickers</div>
      <div style="font-size: 10px; color: #666;">(click to select)</div>
    </div>
    ${legendItems}
    ${showAllButton}
  </div>`;
  
  // Create container with loading overlay and hover elements
  const container = html`<div style="position: relative;">
    <div class="chart-loading-overlay" style="display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; 
         background: rgba(255,255,255,0.7); z-index: 10; align-items: center; justify-content: center;">
      <div style="text-align: center;">
        <div class="spinner-border text-primary" role="status"></div>
        <div style="margin-top: 10px; font-weight: bold;">Updating metrics...</div>
      </div>
    </div>
    <h5 class="chart-title">Security Return Over Time (${yearsDiff.toFixed(1)} yrs)</h5>
    <div style="position: relative; margin-top: 15px;">
      ${plotContainer}
      ${legendContainer}
    </div>
    <div class="hover-tooltip" style="position: absolute; display: none; background: white; 
         border: 1px solid #ddd; border-radius: 3px; padding: 8px; pointer-events: none;
         box-shadow: 0 2px 4px rgba(0,0,0,0.1); z-index: 20; font-size: 14px;"></div>
  </div>`;
  
  const loadingOverlay = container.querySelector(".chart-loading-overlay");
  const hoverTooltip = container.querySelector(".hover-tooltip");
  
  /* ─── Add Click Interaction for Date Selection ─────── */
  plot.addEventListener("click", ev => {
    const { left } = plot.getBoundingClientRect();
    const px = ev.clientX - left;
    const date = plot.scale("x").invert(px);
    if (!date || isNaN(date)) return;
    
    // Find nearest data point from any series
    const allDataPoints = series.flatMap(s => s.values);
    const closest = allDataPoints.reduce((best, d) => 
      Math.abs(d.date - date) < Math.abs(best.date - date) ? d : best, 
      { date: new Date(0) }
    );
    
    // Show loading overlay
    loadingOverlay.style.display = "flex";
    
    // Update selected date and trigger event
    const input = viewof selectedEndDate;
    input.value = closest.date.toISOString();
    
    // Update UI
    requestAnimationFrame(() => {
      input.dispatchEvent(new CustomEvent("input", { bubbles: true }));
      setTimeout(() => loadingOverlay.style.display = "none", 100);
    });
  });
  
  return container;
}
Code
// Calculate portfolio projection data for use in both summary and chart
function calculateProjectionData() {
  if (!enrichedSecurityData || enrichedSecurityData.length === 0) {
    return { error: "No portfolio data available for projection" };
  }
  
  // Expected returns for each security
  const expectedReturns = {};
  let portfolioExpectedReturn = 0;
  
  // Get expected returns from enrichedSecurityData if available
  // Otherwise use default expected return from er_optimal
  enrichedSecurityData.forEach(security => {
    // Use either security's expected return or asset class average
    let securityReturn;
    if (security.Type === "Equity") {
      securityReturn = security["Expected Return"] ? 
        parseFloat(security["Expected Return"]) : (calc_equity_return || 0.237);
    } else if (security.Type === "Bond") {
      securityReturn = bond_ret || 0.04;
    } else if (security.Type === "Risk-Free") {
      securityReturn = rf_rate;
    } else {
      securityReturn = 0.05; // Default fallback
    }
    
    expectedReturns[security.Ticker] = securityReturn;
    portfolioExpectedReturn += security.completeWeight * securityReturn;
  });
  
  // Use er_optimal if we couldn't calculate a weighted expected return
  if (portfolioExpectedReturn === 0) {
    portfolioExpectedReturn = er_optimal || 0.08;
  }
  
  // Calculate compound growth over time horizon with annual compounding
  const yearsArray = Array.from({ length: Math.ceil(time_horizon) + 1 }, (_, i) => i);
  
  // Initial uncertainty bound percentage (5%)
  const initialBoundPct = 0.05;
  // Annual compound growth rate for uncertainty (10%)
  const boundGrowthRate = 0.10;
  
  // Calculate base projections with compounding uncertainty bounds
  const projectionData = yearsArray.map(year => {
    // Calculate projected value based on expected return
    const projectedValue = investment_amount * Math.pow(1 + portfolioExpectedReturn, year);
    
    // Calculate uncertainty percentage for this year (compounded)
    // Year 0 has no uncertainty (it's the current value)
    const boundPct = year === 0 ? 0 : initialBoundPct * Math.pow(1 + boundGrowthRate, year - 1);
    
    // Apply uncertainty bounds
    const upperBound = projectedValue * (1 + boundPct);
    const lowerBound = projectedValue * (1 - boundPct);
    
    return {
      year,
      value: projectedValue,
      upperBound,
      lowerBound,
      boundPct: boundPct * 100, // Store for display
      gain: projectedValue - investment_amount,
      returnPct: (projectedValue / investment_amount - 1) * 100
    };
  });

  const finalValue = projectionData[projectionData.length - 1].value;
  const finalUpper = projectionData[projectionData.length - 1].upperBound;
  const finalLower = projectionData[projectionData.length - 1].lowerBound;
  const finalBoundPct = projectionData[projectionData.length - 1].boundPct;
  const totalReturn = (finalValue / investment_amount - 1) * 100;
  const annualizedReturn = portfolioExpectedReturn * 100;
  
  return {
    projectionData,
    finalValue,
    finalUpper,
    finalLower,
    finalBoundPct,
    totalReturn,
    annualizedReturn,
    portfolioExpectedReturn
  };
};
Code
viewof portfolioProjectionSummary = {
  const projData = calculateProjectionData();
  if (projData.error) {
    return html`<div class="alert alert-warning">${projData.error}</div>`;
  }
  
  const {finalValue, finalUpper, finalLower, finalBoundPct, totalReturn, annualizedReturn} = projData;
  
  // Create a container for our component that persists between renders
  const container = html`<div class="portfolio-projection-container"></div>`;
  
  // Use a global variable to track the state that persists between renders
  window.portfolioState = window.portfolioState || {
    selectedPortfolioType: "Optimal Portfolio",
    selectedFeeType: "Annual Only"
  };
  
  // Create custom styled radio buttons to ensure they maintain state
  const portfolioTypeRadio = html`
    <div class="form-group mb-3">
      <div class="radio-container">
        <label class="form-label fw-bold portfolio-label">Portfolio:</label>
        <div class="custom-radio-group">
          <div class="form-label radio-options">
            <label class="custom-radio ${window.portfolioState.selectedPortfolioType === "Optimal Portfolio" ? "selected" : ""}">
              <input type="radio" name="portfolioType" value="Optimal Portfolio" 
                ${window.portfolioState.selectedPortfolioType === "Optimal Portfolio" ? "checked" : ""}>
              <span class="radio-label">Optimal</span>
            </label>
            
            <label class="custom-radio ${window.portfolioState.selectedPortfolioType === "Minimum Variance" ? "selected" : ""}">
              <input type="radio" name="portfolioType" value="Minimum Variance" 
                ${window.portfolioState.selectedPortfolioType === "Minimum Variance" ? "checked" : ""}>
              <span class="radio-label">Minimum Variance</span>
            </label>
            
            <label class="custom-radio ${window.portfolioState.selectedPortfolioType === "Maximum Variance" ? "selected" : ""}">
              <input type="radio" name="portfolioType" value="Maximum Variance" 
                ${window.portfolioState.selectedPortfolioType === "Maximum Variance" ? "checked" : ""}>
              <span class="radio-label">Maximum Variance</span>
            </label>
          </div>
        </div>
      </div>
    </div>`;

  // Create fee structure type radio buttons
  const feeTypeRadio = html`
    <div class="form-group mb-3">
      <div class="radio-container">
        <label class="form-label fw-bold portfolio-label">Fee Structure:</label>
        <div class="custom-radio-group">
          <div class="form-label radio-options">
            <label class="custom-radio ${window.portfolioState.selectedFeeType === "Annual Only" ? "selected" : ""}">
              <input type="radio" name="feeType" value="Annual Only" 
                ${window.portfolioState.selectedFeeType === "Annual Only" ? "checked" : ""}>
              <span class="radio-label">Annual Only</span>
            </label>
            
            <label class="custom-radio ${window.portfolioState.selectedFeeType === "Front-Load" ? "selected" : ""}">
              <input type="radio" name="feeType" value="Front-Load" 
                ${window.portfolioState.selectedFeeType === "Front-Load" ? "checked" : ""}>
              <span class="radio-label">Front-Load</span>
            </label>
            
            <label class="custom-radio ${window.portfolioState.selectedFeeType === "Back-Load" ? "selected" : ""}">
              <input type="radio" name="feeType" value="Back-Load" 
                ${window.portfolioState.selectedFeeType === "Back-Load" ? "checked" : ""}>
              <span class="radio-label">Back-Load</span>
            </label>
          </div>
        </div>
      </div>
    </div>`;
  
  // Define fixed fee values
  const frontLoadValue = 0.03;  
  const backLoadValue = 0.02;   
  const mgmtFeeValues = {
    "Annual Only": 0.009,
    "Front-Load": 0.002,
    "Back-Load": 0.003
  };
  const txnCostValue = 0.002;
  
  // Function to update portfolio type
  function updatePortfolioType(portfolioType) {
    console.log(`Setting portfolio type to: ${portfolioType}`);
    window.portfolioState.selectedPortfolioType = portfolioType;
    
    // Update visual state of radio buttons
    portfolioTypeRadio.querySelectorAll('.custom-radio').forEach(radio => {
      const radioInput = radio.querySelector('input');
      if (radioInput.value === portfolioType) {
        radio.classList.add('selected');
        radioInput.checked = true;
      } else {
        radio.classList.remove('selected');
        radioInput.checked = false;
      }
    });
    
    // Find matching portfolio row if possible
    applyPortfolioSelection(portfolioType);
    
    // Update the UI with the new state
    renderContent();
  }
  
  // Function to update fee type
  function updateFeeType(feeType) {
    window.portfolioState.selectedFeeType = feeType;
    
    // Update visual state of radio buttons
    feeTypeRadio.querySelectorAll('.custom-radio').forEach(radio => {
      const radioInput = radio.querySelector('input');
      if (radioInput.value === feeType) {
        radio.classList.add('selected');
        radioInput.checked = true;
      } else {
        radio.classList.remove('selected');
        radioInput.checked = false;
      }
    });
    
    // Update the UI with the new state
    renderContent();
  }
  
  // Function to find and apply the selected portfolio in tables
  function applyPortfolioSelection(portfolioType) {
    const tables = document.querySelectorAll('.table-responsive');
    let targetRow = null;
    
    // Define search terms for each portfolio type
    const searchTerms = {
      "Optimal Portfolio": ["Optimal", "Sharpe Ratio", "Max Sharpe"],
      "Minimum Variance": ["Minimum", "Min Var", "Min Variance", "Min Risk"],
      "Maximum Variance": ["Maximum", "Max Var", "Max Return", "Max Expected Return"]
    };
    
    // Search through tables for matching rows
    if (tables.length > 0) {
      tables.forEach(table => {
        const rows = table.querySelectorAll('tr[data-idx]');
        
        rows.forEach(row => {
          const firstCell = row.querySelector('td');
          if (firstCell && firstCell.textContent) {
            const cellText = firstCell.textContent.trim();
            
            // Check if any terms for the current portfolio type match
            const terms = searchTerms[portfolioType] || [];
            if (terms.some(term => cellText.toLowerCase().includes(term.toLowerCase()))) {
              targetRow = row;
            }
          }
        });
      });
      
      // Apply the found row if possible
      if (targetRow && targetRow.portfolioData) {
        console.log(`Found matching row for ${portfolioType}:`, targetRow.textContent);
        
        try {
          // Update portfolio weights if possible
          if (typeof viewof fixed_optimal_weight_input !== 'undefined') {
            viewof fixed_optimal_weight_input.value = targetRow.portfolioData.riskyWeight || 0.5;
            viewof fixed_optimal_weight_input.dispatchEvent(new Event("input"));
          }
          
          if (typeof viewof equity_weight !== 'undefined') {
            const equitySlider = viewof equity_weight;
            const sliderInput = equitySlider.querySelector('input');
            if (sliderInput) {
              sliderInput.value = targetRow.portfolioData.equityWeight || 0.5;
              sliderInput.dispatchEvent(new Event("input"));
            }
            
            equitySlider.value = targetRow.portfolioData.equityWeight || 0.5;
            equitySlider.dispatchEvent(new Event("input"));
          }
          
          // Highlight the selected row in all tables
          document.querySelectorAll('tr[data-idx]').forEach(r => r.classList.remove('selected-row'));
          targetRow.classList.add('selected-row');
        } catch (err) {
          console.error("Error applying portfolio selection:", err);
        }
      } else {
        console.log(`Could not find a table row matching "${portfolioType}". Using default values.`);
      }
    }
  }
  
  // Function to render content with the current state
  function renderContent() {
    // Get the selected fee type
    const selectedFeeType = window.portfolioState.selectedFeeType;
    const selectedType = window.portfolioState.selectedPortfolioType;
    
    // Get the appropriate management fee for the selected fee structure
    const mgmtFeeValue = mgmtFeeValues[selectedFeeType];
    
    // Calculate fee scenarios
    const portfolioExpectedReturn = er_optimal || 0.08;
    const yearsArray = Array.from({ length: Math.ceil(time_horizon) + 1 }, (_, i) => i);
    
    // Calculate different fee structure scenarios
    const frontLoadData = calculateFeeScenario("Front-Load", portfolioExpectedReturn, yearsArray, 
                                          frontLoadValue, 0, mgmtFeeValues["Front-Load"], txnCostValue);
    const annualOnlyData = calculateFeeScenario("Annual Only", portfolioExpectedReturn, yearsArray, 
                                          0, 0, mgmtFeeValues["Annual Only"], txnCostValue);
    const backLoadData = calculateFeeScenario("Back-Load", portfolioExpectedReturn, yearsArray, 
                                        0, backLoadValue, mgmtFeeValues["Back-Load"], txnCostValue);
    
    // Select the appropriate fee data based on current selection
    let selectedFeeData;
    if (selectedFeeType === "Front-Load") {
      selectedFeeData = frontLoadData;
    } else if (selectedFeeType === "Back-Load") {
      selectedFeeData = backLoadData;
    } else {
      selectedFeeData = annualOnlyData;
    }
    
    // Calculate total cash fees for the selected structure
    const totalCashFees = selectedFeeData.reduce((sum, row) => sum + row.cashFee, 0);
    
    // Calculate adjusted holding period return accounting for fees
    const adjustedFinalValue = finalValue - totalCashFees;
    const adjustedTotalReturn = ((adjustedFinalValue - investment_amount) / investment_amount) * 100;
    
    // Generate the summary card HTML
    const summaryCard = html`
    <div>
      <h5 class="table-title">Portfolio Projection (${time_horizon} yrs)</h5>

      <!-- Portfolio type controls -->
      <div class="row">
        <div class="col-md-12 mb-0">
          ${portfolioTypeRadio}
        </div>
      </div>

      <!-- Fee structure controls -->
      <div class="row">
        <div class="col-md-12 mb-0">
          ${feeTypeRadio}
        </div>
      </div>

      <!-- Projection summary data -->
      <div class="row w-100 mx-0">
        <div class="col-md-6 px-0">
          <table class="table table-sm mb-0 mt-0">
            <tbody>
              <tr>
                <th>Investment Amount:</th>
                <td>${formatCurrency(investment_amount)}</td>
              </tr>
              <tr>
                <th>Time Horizon:</th>
                <td>${time_horizon} years</td>
              </tr>
              <tr>
                <th>Selected Portfolio:</th>
                <td>${selectedType}</td>
              </tr>
              <tr>
                <th>Fee Structure:</th>
                <td>${selectedFeeType}</td>
              </tr>
            </tbody>
          </table>
        </div>

        <div class="col-md-6 px-0">
          <table class="table table-sm mb-0">
            <tbody>
              <tr>
                <th>Projected Final Value:</th>
                <td>${formatCurrency(finalValue)}</td>
              </tr>
              <tr>
                <th>Uncertainty Range (±${finalBoundPct.toFixed(2)}%):</th>
                <td>${formatCurrency(finalLower)} - ${formatCurrency(finalUpper)}</td>
              </tr>
              <tr>
                <th>Projected Cash Fees:</th>
                <td>${formatCurrency(totalCashFees)}</td>
              </tr>
              <tr>
                <th>Adjusted Final Value:</th>
                <td>${formatCurrency(adjustedFinalValue)}</td>
              </tr>
              <tr>
                <th>Holding Period Return:</th>
                <td>${adjustedTotalReturn.toFixed(2)}%</td>
              </tr>
              <tr>
                <th>IRR (Annualized):</th>
                <td>${annualizedReturn.toFixed(2)}%</td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>`;
    
    // Clear container and append new content
    container.innerHTML = '';
    container.appendChild(summaryCard);
    
    // Re-attach event listeners to the newly created elements
    attachEventListeners();
  }
  
  // Function to attach event listeners
  function attachEventListeners() {
    // Add listeners to portfolio type radio buttons
    portfolioTypeRadio.querySelectorAll('input[name="portfolioType"]').forEach(radio => {
      radio.addEventListener('change', function() {
        updatePortfolioType(this.value);
      });
    });
    
    // Add listeners to fee type radio buttons
    feeTypeRadio.querySelectorAll('input[name="feeType"]').forEach(radio => {
      radio.addEventListener('change', function() {
        updateFeeType(this.value);
      });
    });
  }
  
  // Initial render
  renderContent();
  attachEventListeners();
  
  // Return the container
  return container;
}
Code
function calculateFeeScenario(feeType, portfolioReturn, yearsArray, 
                          frontLoadFeeValue, backLoadFeeValue, managementFeeValue, transactionCostValue) {
  const result = [];
  const totalYears = Math.ceil(time_horizon);
  
  for (let year = 0; year <= totalYears; year++) {
    let valueBefore, valueAfter, cashFee;
    
    if (year === 0) {
      valueBefore = investment_amount;
      
      // Apply initial fees - ONLY for Front-Load
      if (feeType === "Front-Load") {
        cashFee = valueBefore * frontLoadFeeValue;
        valueAfter = valueBefore - cashFee;
      } else {
        // No fees for Annual Only and Back-Load in year 0
        cashFee = 0;
        valueAfter = valueBefore;
      }
    } else {
      // Get previous year's ending value as this year's starting value
      valueBefore = result[year-1].valueAfterFees * (1 + portfolioReturn);
      
      // Apply annual fees
      cashFee = valueBefore * managementFeeValue;
      
      // Apply back-end load in the final year
      if (year === totalYears && feeType === "Back-Load") {
        cashFee += (valueBefore - cashFee) * backLoadFeeValue;
      }
      
      valueAfter = valueBefore - cashFee;
    }
    
    // Calculate opportunity cost for this specific fee
    // This is what the fee would grow to by the end if invested at the risk-free rate
    const yearsRemaining = totalYears - year;
    const oppCost = cashFee * Math.pow(1 + rf_rate, yearsRemaining);
    
    result.push({
      year,
      valueBefore,
      cashFee,
      oppCostFees: oppCost,
      valueAfterFees: valueAfter
    });
  }
  
  return result;
}
Code
// Display portfolio projection chart
viewof portfolioProjectionChart = {
  const projData = calculateProjectionData();
  if (projData.error) {
    return html`<div class="alert alert-warning">${projData.error}</div>`;
  }
  
  const {projectionData} = projData;
  
  // Create the projection chart
  const plot = Plot.plot({
    marginLeft: 90,
    marginRight: 40,
    marginTop: 30,
    marginBottom: 40,
    style: { 
      fontSize: "14px", 
      background: "transparent", 
      overflow: "visible" 
    },
    x: {
      label: "Year",
      // grid: true,
      domain: [0, Math.ceil(time_horizon)]
    },
    y: {
      label: "Portfolio Value ($)",
      grid: true,
      tickFormat: d => `$${d3.format(",")(d.toFixed(0))}`
    },
    marks: [
      // Uncertainty bounds area (±20%)
      Plot.areaY(projectionData, {
        x: "year",
        y1: "lowerBound",
        y2: "upperBound",
        fill: "#28a745",
        fillOpacity: 0.1,
      }),
      // Upper bound line
      Plot.lineY(projectionData, {
        x: "year",
        y: "upperBound",
        stroke: "#28a745",
        strokeOpacity: 0.4,
        strokeWidth: 1.5,
        strokeDasharray: "3 3"
      }),
      // Lower bound line
      Plot.lineY(projectionData, {
        x: "year",
        y: "lowerBound",
        stroke: "#28a745",
        strokeOpacity: 0.4,
        strokeWidth: 1.5,
        strokeDasharray: "3 3"
      }),
      // Main projection line
      Plot.lineY(projectionData, {
        x: "year",
        y: "value",
        stroke: "#28a745",
        strokeWidth: 3
      }),
      // Initial investment reference line
      Plot.ruleY([investment_amount], { 
        stroke: "#6c757d", 
        strokeWidth: 2, 
        strokeDasharray: "4 4" 
      }),
      Plot.text([{year: 0, value: investment_amount}], {
        x: "year",
        y: "value",
        dy: -10,
        text: () => `Initial Investment: ${formatCurrency(investment_amount)}`,
        fontSize: 12
      }),
      // Final value indicator
      Plot.dot(projectionData.filter(d => d.year === Math.ceil(time_horizon)), {
        x: "year",
        y: "value",
        r: 6,
        fill: "#28a745",
        stroke: "white",
        strokeWidth: 2
      }),
      // Final value label
      Plot.text(projectionData.filter(d => d.year === Math.ceil(time_horizon)), {
        x: "year",
        y: "value",
        dy: -15,
        text: d => `${formatCurrency(d.value)}`,
        fontSize: 14,
        fontWeight: "bold"
      }),
      // Upper bound value label
      Plot.text(projectionData.filter(d => d.year === Math.ceil(time_horizon)), {
        x: "year",
        y: "upperBound",
        dy: -5,
        text: d => `Upper: ${formatCurrency(d.upperBound)}`,
        fontSize: 11,
        fill: "#28a745"
      }),
      // Lower bound value label
      Plot.text(projectionData.filter(d => d.year === Math.ceil(time_horizon)), {
        x: "year",
        y: "lowerBound",
        dy: 15,
        text: d => `Lower: ${formatCurrency(d.lowerBound)}`,
        fontSize: 11,
        fill: "#28a745"
      }),
      // Hover interaction elements
      Plot.ruleX(projectionData, Plot.pointerX({
        x: "year", 
        stroke: "#28a745", 
        strokeWidth: 1.5, 
        strokeDasharray: "4 4"
      })),
      Plot.dot(projectionData, Plot.pointerX({
        x: "year", 
        y: "value", 
        fill: "#28a745", 
        stroke: "white", 
        strokeWidth: 2, 
        r: 5
      })),
      // Enhanced tooltip for interactive exploration
      Plot.tip(projectionData, Plot.pointerX({
        x: "year",
        y: "value",
        lineWidth: 35,
        fontSize: 12,
        fontWeight: 700,
        fill: "white",
        stroke: "white",
        strokeWidth: 2,
        lineHeight: 1.3,
        title: d => 
        `Year: ${d.year} 
        \nProjected Value: ${formatCurrency(d.value)}
        \nUncertainty Range (±${d.boundPct.toFixed(2)}%):
  Upper: ${formatCurrency(d.upperBound)}
  Lower: ${formatCurrency(d.lowerBound)}
        \nPerformance:
  Gain: ${formatCurrency(d.gain)}
  Return: ${d.returnPct.toFixed(2)}%`,
      }))
    ]
  });
  
  return plot;
}
Code
function createFeeComparisonTable(yearsArray, frontLoadData, annualOnlyData, backLoadData,
                             frontLoadFeeValue, annualOnlyFeeValue, frontLoadMgmtFeeValue, backLoadMgmtFeeValue, backLoadFeeValue) {
  // Only show first and last years if time horizon > 5 years
  const displayYears = time_horizon > 5 
    ? [0, 1, 2, Math.floor(time_horizon / 2), time_horizon - 1, time_horizon]
    : yearsArray;
  
  // Use Set to eliminate duplicates
  const uniqueYears = [...new Set(displayYears)].sort((a, b) => a - b);
  
  return html`
    <div class="table-responsive">
      <h5 class="table-title">Portfolio Projection + Fee Structure Comparison (${time_horizon} yrs)</h5>
      <table class="table table-sm table-striped">
        <thead>
          <tr>
            <th rowspan="2">Period</th>
            <th colspan="4" style="text-align: center; background-color:rgb(209, 161, 2);">Front-Load (${(frontLoadFeeValue*100).toFixed(1)}% + ${(frontLoadMgmtFeeValue*100).toFixed(2)}% annual)</th>
            <th colspan="4" style="text-align: center; background-color:rgb(0, 158, 144);">Annual Only (${(annualOnlyFeeValue*100).toFixed(2)}% annual)</th>
            <th colspan="4" style="text-align: center; background-color:rgb(151, 23, 0);">Back-Load (${(backLoadFeeValue*100).toFixed(1)}% + ${(backLoadMgmtFeeValue*100).toFixed(2)}% annual)</th>
          </tr>
          <tr>
            <th>Principal before fees</th>
            <th>Cash Fees</th>
            <th>Fees w/ opport. cost</th>
            <th>Principal after fees</th>
            <th>Principal before fees</th>
            <th>Cash Fees</th>
            <th>Fees w/ opport. cost</th>
            <th>Principal after fees</th>
            <th>Principal before fees</th>
            <th>Cash Fees</th>
            <th>Fees w/ opport. cost</th>
            <th>Principal after fees</th>
          </tr>
        </thead>
        <tbody>
          ${uniqueYears.map(year => {
            const frontRow = frontLoadData[year];
            const annualRow = annualOnlyData[year];
            const backRow = backLoadData[year];
            
            return html`
              <tr ${year === time_horizon ? 'style="font-weight: bold;"' : ''}>
                <td>${year}</td>
                <td>${formatCurrency(frontRow.valueBefore)}</td>
                <td>${formatCurrency(frontRow.cashFee)}</td>
                <td>${formatCurrency(frontRow.oppCostFees)}</td>
                <td>${formatCurrency(frontRow.valueAfterFees)}</td>
                <td>${formatCurrency(annualRow.valueBefore)}</td>
                <td>${formatCurrency(annualRow.cashFee)}</td>
                <td>${formatCurrency(annualRow.oppCostFees)}</td>
                <td>${formatCurrency(annualRow.valueAfterFees)}</td>
                <td>${formatCurrency(backRow.valueBefore)}</td>
                <td>${formatCurrency(backRow.cashFee)}</td>
                <td>${formatCurrency(backRow.oppCostFees)}</td>
                <td>${formatCurrency(backRow.valueAfterFees)}</td>
              </tr>
            `;
          })}
          <tr style="background-color: #f8f9fa; font-weight: bold;">
            <td>Total</td>
            <td></td>
            <td>${formatCurrency(frontLoadData.reduce((sum, row) => sum + row.cashFee, 0))}</td>
            <td>${formatCurrency(frontLoadData.reduce((sum, row) => sum + row.oppCostFees, 0))}</td>
            <td>${formatPercent((frontLoadData[frontLoadData.length-1].valueAfterFees / investment_amount - 1))}</td>
            <td></td>
            <td>${formatCurrency(annualOnlyData.reduce((sum, row) => sum + row.cashFee, 0))}</td>
            <td>${formatCurrency(annualOnlyData.reduce((sum, row) => sum + row.oppCostFees, 0))}</td>
            <td>${formatPercent((annualOnlyData[annualOnlyData.length-1].valueAfterFees / investment_amount - 1))}</td>
            <td></td>
            <td>${formatCurrency(backLoadData.reduce((sum, row) => sum + row.cashFee, 0))}</td>
            <td>${formatCurrency(backLoadData.reduce((sum, row) => sum + row.oppCostFees, 0))}</td>
            <td>${formatPercent((backLoadData[backLoadData.length-1].valueAfterFees / investment_amount - 1))}</td>
          </tr>
        </tbody>
      </table>
    </div>
  `;
}
Code
function createFeeComparisonDisplay() {
  // Define fee values
  const frontLoadFeeValue = 0.03;  // 3.0% front load fee
  const backLoadFeeValue = 0.02;   // 2.0% back load fee
  const mgmtFeeValues = {
    "Annual Only": 0.009,      // 0.90% for annual-only
    "Front-Load": 0.002,       // 0.20% for front-load
    "Back-Load": 0.003         // 0.30% for back-load
  };
  const txnCostValue = 0.002;  // 0.20% transaction cost
  
  // Calculate fee scenarios
  const portfolioExpectedReturn = er_optimal || 0.08;
  const yearsArray = Array.from({ length: Math.ceil(time_horizon) + 1 }, (_, i) => i);
  
  // Calculate different fee structure scenarios
  const frontLoadData = calculateFeeScenario("Front-Load", portfolioExpectedReturn, yearsArray, 
                                       frontLoadFeeValue, 0, mgmtFeeValues["Front-Load"], txnCostValue);
  const annualOnlyData = calculateFeeScenario("Annual Only", portfolioExpectedReturn, yearsArray, 
                                       0, 0, mgmtFeeValues["Annual Only"], txnCostValue);
  const backLoadData = calculateFeeScenario("Back-Load", portfolioExpectedReturn, yearsArray, 
                                     0, backLoadFeeValue, mgmtFeeValues["Back-Load"], txnCostValue);
  
  // Call createFeeComparisonTable with all required parameters including separate management fees
  return createFeeComparisonTable(
    yearsArray, 
    frontLoadData, 
    annualOnlyData, 
    backLoadData,
    frontLoadFeeValue, 
    mgmtFeeValues["Annual Only"], 
    mgmtFeeValues["Front-Load"],
    mgmtFeeValues["Back-Load"],
    backLoadFeeValue
  );
}

// Display the fee comparison table
createFeeComparisonDisplay();
Code
html`
  <div style="display: flex; flex-direction: column; height: 100%;">
    <div style="position: absolute; top: 0; left: 0; width: 100%; height: 97%;">
      <iframe 
        width="100%" 
        height="100%" 
        src="https://raw.githack.com/renan-peres/mfin-portfolio-management/refs/heads/main/reports/portfolio_vs_benchmark-2025-04-26.html"
        frameborder="0"
        allowfullscreen
      ></iframe>
    </div>
  </div>
`;
Code
function optimalMetricsTable() {
  /* Guard against missing inputs */
  if (
    sharpe_ratio == null ||
    fixed_optimal_weight == null ||
    er_optimal == null ||
    std_dev_optimal == null ||
    fixed_utility_optimal == null
  ) {
    return html`<div class="alert alert-warning">Summary metrics unavailable</div>`;
  }

  /* Render */
  return html`
    <table class="table-responsive">
      <tbody>
        <tr><td>Sharpe Ratio</td>                 <td>${sharpe_ratio.toFixed(2)}</td></tr>
        <tr><td>Optimal Weight (Risky Pf.)</td>   <td>${formatPercent(fixed_optimal_weight)}</td></tr>
        <tr><td>Optimal Weight (Risk-free Asset)</td>   <td>${formatPercent(1- fixed_optimal_weight)}</td></tr>
        <tr><td>Expected Return @ Optimal</td>    <td>${formatPercent(er_optimal)}</td></tr>
        <tr><td>Std. Deviation @ Optimal</td>     <td>${formatPercent(std_dev_optimal)}</td></tr>
        <tr><td>Utility @ Optimal</td>            <td>${formatPercent(fixed_utility_optimal)}</td></tr>
      </tbody>
    </table>`;
}
Code
function allocationDataTable() {
  /* Guard against missing inputs */
  if (
    fixed_allocation_data == null || 
    fixed_max_utility_idx == null ||
    rf_rate == null ||
    risk_aversion == null
  ) {
    return html`<div class="alert alert-warning">Allocation data unavailable</div>`;
  }

  /* Render table */
  return html`
    <div>
      <table class="table-responsive mt-0 mb-0">
        <thead>
          <tr>
            <th>Weight of Risky Pf</th>
            <th>E(rc) on CAL</th>
            <th>Std Dev (Complete Pf)</th>
            <th>Utility</th>
            <th>E(rc) on Indiff. Curve</th>
          </tr>
        </thead>
        <tbody>
          ${fixed_allocation_data.map((d, i) => {
            // Calculate indifference value: rf_rate + (1/2 * risk_aversion * variance)
            const indiff = fixed_utility_optimal + (1/2 * risk_aversion * Math.pow(d.Standard_Deviation, 2));
            
            return html`
              <tr ${i === fixed_max_utility_idx ? 'style="background-color: #ffff9980;"' : ''}>
                <td>${d.Weight_Pct}</td>
                <td>${formatPercent(d.Expected_Return)}</td>
                <td>${formatPercent(d.Standard_Deviation)}</td>
                <td>${formatPercent(d.Utility)}${i === fixed_max_utility_idx ? ' ★' : ''}</td>
                <td>${formatPercent(indiff)}</td>
              </tr>
            `;
          })}
        </tbody>
      </table>
    </div>`;
}
  • Capital Allocation Line
  • Utility vs. Risky Pf Weight
Code
generateCMLPoints = () => {
  // Create points from risk-free rate to slightly beyond risky portfolio
  const numPoints = 100;
  const maxStdDev = std_dev_risky * 1.2; // Extend slightly beyond risky portfolio
  
  // Generate equally spaced points along the line
  return Array.from({length: numPoints}, (_, i) => {
    // Standard deviation spans from 0 to maxStdDev
    const sd = (i / (numPoints - 1)) * maxStdDev;
    
    // Calculate corresponding expected return using the CML equation
    // CML: E(R) = Rf + (E(Rm) - Rf) / σm * σ
    const er = rf_rate + (er_risky - rf_rate) / std_dev_risky * sd;
    
    return {
      Standard_Deviation: sd,
      Expected_Return: er
    };
  });
};

// Generate indifference curve points using the same formula as in allocationDataTable
generateIndifferencePoints = () => {
  // Create points from 0 to slightly beyond risky portfolio
  const numPoints = 100;
  const maxStdDev = std_dev_risky * 1.2; // Extend slightly beyond risky portfolio
  
  // Generate equally spaced points along the curve
  return Array.from({length: numPoints}, (_, i) => {
    // Standard deviation spans from 0 to maxStdDev
    const sd = (i / (numPoints - 1)) * maxStdDev;
    
    // Calculate corresponding expected return using the same indifference curve equation as in the table
    // Indifference curve: E(R) = fixed_utility_optimal + (1/2 * risk_aversion * sd^2)
    const er = fixed_utility_optimal + (1/2 * risk_aversion * Math.pow(sd, 2));
    
    return {
      Standard_Deviation: sd,
      Expected_Return: er
    };
  });
};

// Calculate dynamic optimal portfolio position using fixed weight and current risky portfolio
calculateDynamicOptimalPoint = () => {
  // Use fixed risk-aversion weight with current risky portfolio characteristics
  const dynamic_std_dev_optimal = fixed_optimal_weight * std_dev_risky;
  const dynamic_er_optimal = (fixed_optimal_weight * er_risky) + ((1 - fixed_optimal_weight) * rf_rate);
  
  return {
    std_dev: dynamic_std_dev_optimal,
    er: dynamic_er_optimal
  };
};

// Get dynamic CML points and optimal portfolio position
cmlPoints = generateCMLPoints();
indifferencePoints = generateIndifferencePoints();
dynamicOptimalPoint = calculateDynamicOptimalPoint();
Code
// Create and display the plot with its header
viewof capitalAllocationDisplay = {
  // Create the plot (using the same configuration you already have)
  const plot = Plot.plot({
    marginBottom: 40,
    marginLeft: 60,
    marginRight: 170, // Increased for legend space
    x: {
      label: "Risk (Standard Deviation)",
      tickFormat: d => d3.format(".1%")(d),
      grid: true
    },
    y: {
      label: "Expected Return", 
      tickFormat: d => d3.format(".1%")(d),
      grid: true
    },
    marks: [
      // Use dynamically generated CML line based on current risky portfolio
      Plot.line(cmlPoints, {
        x: "Standard_Deviation", 
        y: "Expected_Return",
        stroke: assetColors["CML"], 
        strokeWidth: 2.5,
      }),
      // Add indifference curve
      Plot.line(indifferencePoints, {
        x: "Standard_Deviation", 
        y: "Expected_Return",
        stroke: "#009688", 
        strokeWidth: 2,
      }),
      // Add vertical dashed line from bottom to optimal portfolio point
      Plot.ruleX([std_dev_optimal], {
        stroke: "red", 
        strokeDasharray: "4 4",
        strokeWidth: 2,
        y1: 0,
        y2: er_optimal
      }),
      // Add horizontal dashed line from left to optimal portfolio point
      Plot.ruleY([er_optimal], {
        stroke: "red", 
        strokeDasharray: "4 4",
        strokeWidth: 2,
        x1: 0,
        x2: std_dev_optimal
      }),
      Plot.dot([{
        x: 0, 
        y: rf_rate
      }], {
        x: "x",
        y: "y",
        fill: assetColors["Risk-Free"], 
        r: 6,
        tip: true,
        title: d => `Risk-Free Asset\nReturn: ${formatPercent(d.y)}`
      }),
      // Dynamic risky portfolio (changes with equity/bond slider)
      Plot.dot([{
        x: std_dev_risky, 
        y: er_risky
      }], {
        x: "x",
        y: "y",
        stroke: assetColors["Equity"], 
        fill: assetColors["Equity"], 
        r: 6,
        tip: true,
        title: d => `Risky Portfolio\nReturn: ${formatPercent(d.y)}\nRisk: ${formatPercent(d.x)}`
      }),
      // Dynamic optimal portfolio (uses fixed weight but current risky portfolio characteristics)
      Plot.dot([{
        x: std_dev_optimal, 
        y: er_optimal
      }], {
        x: "x",
        y: "y",
        fill: "red",
        r: 8,
        tip: true,
        title: d => `Optimal Portfolio\nRisky Pf Weight: ${formatPercent(fixed_optimal_weight)}\nRisk-Free Weight: ${formatPercent(1- fixed_optimal_weight)}\nReturn: ${formatPercent(d.y)}\nRisk: ${formatPercent(d.x)}`
      }),
      
      // Add text labels for each point
      Plot.text([
        {
          x: 0, 
          y: rf_rate,
          text: "Risk-Free Asset"
        }
      ], {
        x: "x",
        y: "y",
        text: "text",
        dx: 15,
        dy: -10,
        fontWeight: "bold",
        fontSize: 12,
        fill: assetColors["Risk-Free"],
        stroke: "white",
        strokeWidth: 2,
        paintOrder: "stroke",
        textAnchor: "start"
      }),
      
      Plot.text([
        {
          x: std_dev_risky, 
          y: er_risky,
          text: "Risky Portfolio"
        }
      ], {
        x: "x",
        y: "y",
        text: "text",
        dx: 10,
        dy: -12,
        fontWeight: "bold",
        fontSize: 12,
        fill: assetColors["Equity"],
        stroke: "white",
        strokeWidth: 2,
        paintOrder: "stroke",
        textAnchor: "start"
      }),
      
      Plot.text([
        {
          x: std_dev_optimal, 
          y: er_optimal,
          text: "Optimal Portfolio"
        }
      ], {
        x: "x",
        y: "y",
        text: "text",
        dx: 10,
        dy: -12,
        fontWeight: "bold",
        fontSize: 12,
        fill: "red",
        stroke: "white",
        strokeWidth: 2,
        paintOrder: "stroke",
        textAnchor: "start"
      }),
      
      // Add interactive pointer tracking line
      Plot.ruleX(cmlPoints, Plot.pointerX({
        x: "Standard_Deviation",
        stroke: "#666", 
        strokeWidth: 1, 
        strokeDasharray: "4 4"
      })),
      
      // Add tooltip markers that appear at cursor position
      Plot.dot(cmlPoints, Plot.pointerX({
        x: "Standard_Deviation", 
        y: "Expected_Return",
        fill: assetColors["CML"],
        stroke: "white",
        strokeWidth: 2,
        r: 5
      })),
      
      // Add tooltip marker for indifference curve
      Plot.dot(indifferencePoints, Plot.pointerX({
        x: "Standard_Deviation", 
        y: "Expected_Return",
        fill: "#009688",
        stroke: "white",
        strokeWidth: 2,
        r: 5
      })),
      
      // Add tooltip for CML values
      Plot.tip(cmlPoints, Plot.pointerX({
        x: "Standard_Deviation",
        y: "Expected_Return",
        title: d => {
          // Calculate portfolio weights at this risk level
          const weight = d.Standard_Deviation / std_dev_risky;
          return [
            `Risk (SD of Complete Pf): ${formatPercent(d.Standard_Deviation)}`,
            `Return (CAL): ${formatPercent(d.Expected_Return)}`,
            `Return (Indiff. Curve): ${formatPercent(fixed_utility_optimal + (1/2 * risk_aversion * Math.pow(d.Standard_Deviation, 2)))}`,
            `Risky Portfolio Weight: ${formatPercent(weight)}`,
            `Risk-Free Asset Weight: ${formatPercent(1 - weight)}`
          ].join("\n");
        }
      }))
    ]
  });
  
  
  // Create legend items
  const createLegendItem = (color, label, lineStyle = null, dotStyle = null) => {
    const item = html`<div class="legend-item" style="display: flex; align-items: center; margin-bottom: 8px;">`;
    
    // Add color box/line based on type
    if (lineStyle) {
      // For lines, create a small line segment
      const line = html`<div style="width: 20px; height: 2px; background: ${color}; 
                      ${lineStyle === 'dashed' ? 'border-top: 2px dashed ' + color + '; height: 0;' : ''}
                      margin-right: 8px;"></div>`;
      item.appendChild(line);
    } else if (dotStyle) {
      // For dots, create a small circle
      const dot = html`<div style="width: 10px; height: 10px; border-radius: 50%; background: ${color}; 
                     margin-right: 8px;"></div>`;
      item.appendChild(dot);
    }
    
    // Add label
    const labelElement = html`<div>${label}</div>`;
    item.appendChild(labelElement);
    
    return item;
  };
  
  // Create legend container
  const legendContainer = html`<div class="chart-legend" style="position: absolute; top: 20px; right: 10px; 
                            background: rgba(255,255,255,0.9); border: 1px solid #ddd; border-radius: 4px; 
                            padding: 8px; font-size: 12px; z-index: 5;">
    <div style="font-weight: bold; margin-bottom: 8px;">Legend</div>
    ${createLegendItem(assetColors["CML"], "Capital Market Line", "solid")}
    ${createLegendItem("#009688", "Indifference Curve", "solid")}
    ${createLegendItem("red", "Optimal Portfolio", null, "dot")}
    ${createLegendItem(assetColors["Equity"], "Risky Portfolio", null, "dot")}
    ${createLegendItem(assetColors["Risk-Free"], "Risk-Free Asset", null, "dot")}
    ${createLegendItem("red", "Optimal Allocation", "dashed")}
  </div>`;
  
  // Return container with header and plot
  return html`
    <div class="chart-title">
      <h5 class="chart-title">Capital Allocation Line with Indifference Curve</h5>
      <div style="position: relative;">
        ${plot}
        ${legendContainer}
      </div>
    </div>
  `;
}
Code
html`
  <div>
    <h5 class="table-title">Capital Allocation (Optimal Portfolio)</h5>
    <table class="table table-sm mt-0 mb-0">
    ${optimalMetricsTable()}
    </table>
  </div>`;
Code
// Create and display the chart with header
viewof utilityWeightChart = {
  const plot = Plot.plot({
    marginBottom: 40,
    marginLeft: 60,
    marginRight: 40,
    x: {
      label: "Weight of Risky Portfolio", 
      domain: [0, 1],
      grid: true,
      tickFormat: d => d3.format(".1%")(d)
    },
    y: {
      label: "Utility", 
      grid: true,
      tickFormat: d => d3.format(".1%")(d)
    },
    marks: [
      Plot.line(fixed_chart_data, {
        x: "Weight", 
        y: "Utility",
        stroke: assetColors["CML"], 
        strokeWidth: 2.5,
      }),
      Plot.ruleX(
        [fixed_optimal_weight], 
        {
          stroke: assetColors["Equity"], 
          strokeDasharray: "4 4",
          strokeWidth: 2
        }
      ),
      Plot.dot([{
        x: fixed_optimal_weight, 
        y: fixed_utility_optimal
      }], {
        x: "x",
        y: "y",
        fill: assetColors["Equity"],
        r: 8,
        tip: true,
        title: d => `Optimal Weight: ${formatPercent(d.x)}\nUtility: ${formatPercent(d.y)}`
      }),
      // Add text label to the optimal point
      Plot.text([{
        x: fixed_optimal_weight, 
        y: fixed_utility_optimal,
        text: "Optimal Risky Pf Weight"
      }], {
        x: "x",
        y: "y",
        text: "text",
        dx: -75,
        dy: -15,
        fontWeight: "bold",
        fontSize: 14,
        fill: assetColors["Equity"],
        textAnchor: "start"
      }),
      
      // Add interactive pointer tracking line
      Plot.ruleX(fixed_chart_data, Plot.pointerX({
        x: "Weight",
        stroke: "#666", 
        strokeWidth: 1, 
        strokeDasharray: "4 4"
      })),
      
      // Add tooltip marker at cursor position
      Plot.dot(fixed_chart_data, Plot.pointerX({
        x: "Weight", 
        y: "Utility",
        fill: assetColors["CML"],
        stroke: "white",
        strokeWidth: 2,
        r: 5
      })),
      
      // Add tooltip for values at cursor position
      Plot.tip(fixed_chart_data, Plot.pointerX({
        x: "Weight",
        y: "Utility",
        title: d => [
          `Risky Portfolio Weight: ${formatPercent(d.Weight)}`,
          `Utility: ${formatPercent(d.Utility)}`,
          `Distance from Optimal: ${formatPercent(Math.abs(d.Weight - fixed_optimal_weight))}`
        ].join("\n")
      }))
    ]
  });
  
  // Return container with header and plot
  return html`
    <div class="chart-title">
      <h5 class="chart-title">Utility vs. Risky Portfolio Weight</h5>
      ${plot}
    </div>
  `;
}
Code
// Call the function to render the allocation data table
allocationDataTable()
Code
/* pass howMany = 3 to show only the first three rows */
function assetClassAllocationTable(howMany = Infinity) {
  const { allocations } = dynamicAllocations;
  const rfWeight = 1 - fixed_optimal_weight;

  /* key‑portfolio indices */
  const maxSharpeIdx = allocations.reduce((m, d, i, a) => d.sharpe          > a[m].sharpe          ? i : m, 0);
  const minVarIdx    = allocations.reduce((m, d, i, a) => d.std_dev         < a[m].std_dev         ? i : m, 0);
  const maxReturnIdx = allocations.reduce((m, d, i, a) => d.expected_return > a[m].expected_return ? i : m, 0);

  /* display order */
  const orderedIdx = [
    maxSharpeIdx, minVarIdx, maxReturnIdx,
    ...allocations.map((_, i) => i)
  ].filter((v, i, a) => a.indexOf(v) === i)            // dedupe
   .slice(0, howMany);                                 // ← limit rows here

  /* ── render table ───────────────────────────────────── */
  const table = html`
    <div style="overflow-y: auto;">
      <table class="table-responsive">
      <p>
        <thead>
          <tr>
            <th>Portfolio</th>
            <th>Weights</th>
            <th>Expected Return</th>
            <th>Standard Deviation</th>
            <th>Sharpe Ratio</th>
          </tr>
        </thead>
        <tbody>
          ${orderedIdx.map((idx, rowPos) => {
            const d = allocations[idx];

            /* complete‑portfolio weights */
            const rf    = rfWeight * 100;
            const bond  = (1 - rfWeight) * d.bond_weight;
            const equity= (1 - rfWeight) * d.equity_weight;

            /* expected return */
            const expRet = (rf/100)*rf_rate + (bond/100)*bond_ret + (equity/100)*calc_equity_return;

            /* std dev */
            const stdDev = Math.sqrt(
              Math.pow(bond/100 * bond_vol, 2) +
              Math.pow(equity/100 * calc_equity_std, 2) +
              2*(bond/100)*(equity/100)*bond_vol*calc_equity_std*corr_eq_bd
            );

            /* portfolio name + highlight */
            const prefix = `Portfolio ${rowPos + 1}`;
            let name = prefix;
            let bg   = "";

            if (idx === maxSharpeIdx) { name = `${prefix}: Optimal`;           bg = "#ffff9980"; }
            else if (idx === minVarIdx) { name = `${prefix}: Minimum Variance`;  }
            else if (idx === maxReturnIdx) { name = `${prefix}: Maximum Variance`; }

            // Create row with click handler
            const row = html`
              <tr style="background-color:${bg}; cursor:pointer" data-idx="${idx}" data-row-pos="${rowPos}">
                <td>${name}</td>
                <td>
                  RF: ${rf.toFixed(1)}%, 
                  Bond: ${bond.toFixed(1)}%, 
                  Equity: ${equity.toFixed(1)}%
                </td>
                <td>${formatPercent(expRet)}</td>
                <td>${formatPercent(stdDev)}</td>
                <td>${d.sharpe.toFixed(2)}${idx === maxSharpeIdx ? " ★" : ""}</td>
              </tr>`;
            
            // Store the portfolio data for use in click handler
            row.portfolioData = {
              idx,
              rfPercent: rf,
              bondPercent: bond,
              equityPercent: equity,
              riskyWeight: 1 - (rf/100),
              equityWeight: d.equity_weight / 100,
              bondWeight: d.bond_weight / 100,
              expRet,
              stdDev,
              sharpe: d.sharpe
            };
            
            return row;
          })}
        </tbody>
      </table>
    </p>
    </div>`;
    
  // Add click handlers to all rows
  const rows = table.querySelectorAll('tr[data-idx]');
  rows.forEach(row => {
    row.addEventListener('click', function() {
      // Get portfolio data from the row
      const portfolioData = this.portfolioData;
      
      // Update fixed_optimal_weight_input first
      viewof fixed_optimal_weight_input.value = portfolioData.riskyWeight;
      viewof fixed_optimal_weight_input.dispatchEvent(new Event("input"));
      
      // Important: For the equity slider, we need to update both the DOM element and dispatch the event
      const equitySlider = viewof equity_weight;
      
      if (equitySlider) {
        // Update the underlying input element
        const sliderInput = equitySlider.querySelector('input');
        if (sliderInput) {
          sliderInput.value = portfolioData.equityWeight;
          
          // Trigger the input event on the slider input to update labels
          sliderInput.dispatchEvent(new Event("input"));
        }
        
        // Also update the viewof value and dispatch event
        equitySlider.value = portfolioData.equityWeight;
        equitySlider.dispatchEvent(new Event("input"));
      }
      
      // Highlight the selected row
      rows.forEach(r => r.classList.remove('selected-row'));
      this.classList.add('selected-row');
    });
  });
  
  return table;
}
  • Asset Class Distribution
  • Efficient Frontier
Code
viewof portfolioBarChart = {
  const data = dynamicAllocations;

  // Bar‑chart data with standard hyphen in "Risk-Free" to match constants.js
  const barData = [
    { asset: "Risk-Free", weight: (1 - fixed_optimal_weight) * 100 },
    { asset: "Bond",      weight:  fixed_optimal_weight * bond_weight   * 100 },
    { asset: "Equity",    weight:  fixed_optimal_weight * equity_weight * 100 },
  ];

  /* Sort a fresh copy in descending order of weight */
  const barDataSorted = [...barData].sort((a, b) => b.weight - a.weight);

  return Plot.plot({
    marginTop: 40,
    marginLeft: 60,
    x: {
      label: "",
      domain: barDataSorted.map(d => d.asset),
      padding: 0.3
    },
    y: {
      label: "Weight (%)",
      domain: [0, 100],
      grid: true,
      tickFormat: d => d + "%"
    },
    marks: [
      Plot.barY(barDataSorted, {
        x: "asset",
        y: "weight",
        fill: d => assetColors[d.asset],  // This will now correctly match the keys in constants.js
        tip: true,
        title: d => `${d.asset}: ${d.weight.toFixed(1)}%`
      }),
      Plot.text(barDataSorted, {
        x: "asset",
        y: "weight",
        text: d => `${d.weight.toFixed(1)}%`,
        dy: -10,
        fontWeight: "bold",
        fontSize: 16
      })
    ]
  });
}
Code
assetClassAllocationTable()
Code
// Efficient Frontier Plot
viewof efficientFrontierPlot = {
  // Check if data is available
  if (!dynamicAllocations || !dynamicAllocations.efficient_frontier) {
    return html`<div class="alert alert-warning">Loading data...</div>`;
  }
  
  // Get data from dynamic calculations
  const frontier = dynamicAllocations.efficient_frontier;
  const minVar = dynamicAllocations.min_variance;
  const maxSharpe = dynamicAllocations.max_sharpe;
  const allocations = dynamicAllocations.allocations;
  
  // Create the plot with interactive elements
  const plot = Plot.plot({
    marginBottom: 40,
    marginLeft: 60,
    marginRight: 170, // Increased for legend/labels space
    style: {
      fontSize: "14px",
      background: "transparent"
    },
    x: {
      label: "Risk (Standard Deviation)",
      domain: [0, Math.max(...frontier.map(d => d.Risk)) * 1.1],
      grid: true,
      tickFormat: d => d3.format(".1%")(d)
    },
    y: {
      label: "Expected Return",
      domain: [rf_rate * 0.9, Math.max(...frontier.map(d => d.Return)) * 1.1],
      grid: true,
      tickFormat: d => d3.format(".1%")(d)
    },
    marks: [
      // Capital Allocation Line (CAL)
      Plot.line([
        {x: 0, y: rf_rate},
        {x: maxSharpe.Risk * 1.5, y: rf_rate + (maxSharpe.Return - rf_rate) / maxSharpe.Risk * (maxSharpe.Risk * 1.5)}
      ], {
        x: "x",
        y: "y",
        stroke: assetColors["CML"], 
        strokeWidth: 2.5,
      }),
      
      // Efficient frontier
      Plot.line(frontier, {
        x: "Risk", 
        y: "Return",
        stroke: "#0000ff", 
        strokeWidth: 3,
        strokeDasharray: "4 4",
        curve: "basis"
      }),
      
      // Individual allocations
      Plot.dot(allocations, {
        x: d => d.std_dev,
        y: d => d.expected_return,
        r: 6,
        fill: assetColors["Equity"], 
        stroke: "#0000ff",
        strokeWidth: 1,
        tip: true,
        title: d => `Bond: ${d.bond_weight}%, Equity: ${d.equity_weight}%\nReturn: ${formatPercent(d.expected_return)}\nRisk: ${formatPercent(d.std_dev)}\nSharpe: ${d.sharpe.toFixed(2)}`
      }),
      
      // Min variance portfolio
      Plot.dot([minVar], {
        x: "Risk",
        y: "Return",
        fill: "purple",
        r: 8,
        stroke: "white",
        strokeWidth: 1.5,
        tip: true,
        title: d => `Min Variance Portfolio\nBond: ${(d.Weights[0]*100).toFixed(1)}%, Equity: ${(d.Weights[1]*100).toFixed(1)}%\nReturn: ${formatPercent(d.Return)}\nRisk: ${formatPercent(d.Risk)}\nSharpe: ${d.Sharpe.toFixed(2)}`
      }),

      // Max Sharpe portfolio
      Plot.dot([maxSharpe], {
        x: "Risk",
        y: "Return",
        fill: "red",
        r: 10,
        stroke: "white", 
        strokeWidth: 1.5,
        tip: true,
        title: d => `Max Sharpe Portfolio\nBond: ${(d.Weights[0]*100).toFixed(1)}%, Equity: ${(d.Weights[1]*100).toFixed(1)}%\nReturn: ${formatPercent(d.Return)}\nRisk: ${formatPercent(d.Risk)}\nSharpe: ${d.Sharpe.toFixed(2)}`
      }),
      
      // Risk-free asset
      Plot.dot([{x: 0, y: rf_rate}], {
        x: "x",
        y: "y",
        fill: assetColors["Risk-Free"],
        r: 7,
        stroke: "white",
        strokeWidth: 1.5,
        tip: true,
        title: `Risk-Free Asset\nReturn: ${formatPercent(rf_rate)}`
      }),
      
      // Add text labels with consistent styling
      Plot.text([
        {x: 0, y: rf_rate, text: "Risk-Free Asset"}
      ], {
        x: "x",
        y: "y",
        text: "text",
        dx: 15,
        dy: -10,
        fontWeight: "bold",
        fontSize: 12,
        fill: assetColors["Risk-Free"],
        stroke: "white",
        strokeWidth: 2,
        paintOrder: "stroke",
        textAnchor: "start"
      }),
      
      Plot.text([
        {x: minVar.Risk, y: minVar.Return, text: "Min Variance Portfolio"}
      ], {
        x: "x",
        y: "y",
        text: "text",
        dx: 10,
        dy: -12,
        fontWeight: "bold",
        fontSize: 12,
        fill: "purple",
        stroke: "white",
        strokeWidth: 2,
        paintOrder: "stroke",
        textAnchor: "start"
      }),
      
      Plot.text([
        {x: maxSharpe.Risk, y: maxSharpe.Return, text: "Max Sharpe Portfolio"}
      ], {
        x: "x",
        y: "y",
        text: "text",
        dx: 10,
        dy: -12,
        fontWeight: "bold",
        fontSize: 12,
        fill: "red",
        stroke: "white",
        strokeWidth: 2,
        paintOrder: "stroke",
        textAnchor: "start"
      }),
      
      // Interactive elements
      
      // Add vertical tracking line (x-axis)
      Plot.ruleX(frontier, Plot.pointerX({
        x: "Risk",
        stroke: "#666", 
        strokeWidth: 1, 
        strokeDasharray: "4 4"
      })),
      
      // Add horizontal tracking line (y-axis)
      Plot.ruleY(frontier, Plot.pointer({
        y: "Return",
        stroke: "gray", 
        strokeWidth: 1,
        strokeDasharray: "3,3"
      })),
      
      // Add tooltip marker for frontier point
      Plot.dot(frontier, Plot.pointerX({
        x: "Risk", 
        y: "Return",
        fill: "#0000ff",
        stroke: "white",
        strokeWidth: 2,
        r: 5
      })),
      
      // Add tooltip marker for CAL
      Plot.dot([
        {x: 0, y: rf_rate},
        {x: maxSharpe.Risk * 1.5, y: rf_rate + (maxSharpe.Return - rf_rate) / maxSharpe.Risk * (maxSharpe.Risk * 1.5)}
      ], Plot.pointerX({
        x: "x", 
        y: "y",
        fill: assetColors["CML"],
        stroke: "white",
        strokeWidth: 2,
        r: 5
      }))
    ]
  });
  
  // Create legend items
  const createLegendItem = (color, label, lineStyle = null, dotStyle = null) => {
    const item = html`<div class="legend-item" style="display: flex; align-items: center; margin-bottom: 8px;">`;
    
    // Add color box/line based on type
    if (lineStyle) {
      // For lines, create a small line segment
      const line = html`<div style="width: 20px; height: 2px; background: ${color}; 
                      ${lineStyle === 'dashed' ? 'border-top: 2px dashed ' + color + '; height: 0;' : ''}
                      margin-right: 8px;"></div>`;
      item.appendChild(line);
    } else if (dotStyle) {
      // For dots, create a small circle
      const dot = html`<div style="width: 10px; height: 10px; border-radius: 50%; background: ${color}; 
                     margin-right: 8px;"></div>`;
      item.appendChild(dot);
    }
    
    // Add label
    const labelElement = html`<div>${label}</div>`;
    item.appendChild(labelElement);
    
    return item;
  };
  
  // Create legend container
  const legendContainer = html`<div class="chart-legend" style="position: absolute; top: 20px; right: 10px; 
                            background: rgba(255,255,255,0.9); border: 1px solid #ddd; border-radius: 4px; 
                            padding: 8px; font-size: 12px; z-index: 5;">
    <div style="font-weight: bold; margin-bottom: 8px;">Legend</div>
    ${createLegendItem(assetColors["CML"], "Capital Allocation Line", "solid")}
    ${createLegendItem("#0000ff", "Efficient Frontier", "dashed")}
    ${createLegendItem("red", "Max Sharpe Portfolio", null, "dot")}
    ${createLegendItem("purple", "Min Variance Portfolio", null, "dot")}
    ${createLegendItem(assetColors["Equity"], "Portfolio Allocations", null, "dot")}
    ${createLegendItem(assetColors["Risk-Free"], "Risk-Free Asset", null, "dot")}
  </div>`;
  
  // Return container with header and plot
  return html`
    <div class="chart-title">
      <h5 class="chart-title">Efficient Frontier with Capital Allocation Line</h5>
      <div style="position: relative;">
        ${plot}
        ${legendContainer}
      </div>
    </div>
  `;
}
Code
function createEfficientFrontierTable() {
  const allocations = dynamicAllocations.allocations;
  const complete = dynamicAllocations.complete_portfolio;
  const rfWeight = (1 - fixed_optimal_weight);
  
  // Find index of maximum Sharpe ratio
  const maxSharpeIdx = allocations.reduce((maxIdx, curr, idx, arr) => 
    curr.sharpe > arr[maxIdx].sharpe ? idx : maxIdx, 0);
  
  return html`
    <div style="height: 400px; overflow-y: auto;">
      <table class="table-responsive">
        <thead>
          <tr>
            <th>Risk-Free Weight</th>
            <th>Bond Weight</th>
            <th>Equity Weight</th>
            <th>Expected Return</th>
            <th>Standard Deviation</th>
            <th>Sharpe Ratio</th>
          </tr>
        </thead>
        <tbody>
          ${allocations.map((d, i) => {
            // Calculate weights in the complete portfolio context
            const completeRfWeight = rfWeight * 100;
            const completeBondWeight = (1 - rfWeight) * d.bond_weight;
            const completeEquityWeight = (1 - rfWeight) * d.equity_weight;
            
            return html`
              <tr ${i === maxSharpeIdx ? 'style="background-color: #ffff9980;"' : ''}>
                <td>${completeRfWeight.toFixed(1)}%</td>
                <td>${completeBondWeight.toFixed(1)}% (${d.bond_weight}% of Risky Pf)</td>
                <td>${completeEquityWeight.toFixed(1)}% (${d.equity_weight}% of Risky Pf)</td>
                <td>${formatPercent(d.expected_return)}</td>
                <td>${formatPercent(d.std_dev)}</td>
                <td>${d.sharpe.toFixed(2)}${i === maxSharpeIdx ? ' ★' : ''}</td>
              </tr>
            `;
          })}
        </tbody>
      </table>
    </div>
  `;
}

// Display the table
createEfficientFrontierTable()
Code
function securityAllocationTable() {
  if (!enrichedSecurityData || enrichedSecurityData.length === 0)
    return html`<div>No ticker data available</div>`;

  // Get ticker prices from processed quotes data
  const tickerPrices = processQuotesData(daily_quotes, equity_tickers) ?? {};

  /* ─── HTML table ──────────────────────────────────────── */
  return html`
    <div style="overflow-y: auto;">
      <table class="table-responsive">
        <thead>
          <tr>
            <th>Asset Class</th>
            <th>Ticker</th>
            <th>Name</th>
            <th>Sector</th>
            <th>Asset Class&nbsp;Weight</th>
            <th>Risky Pf&nbsp;Weight</th>
            <th>Complete Pf&nbsp;Weight</th>
            <th>Last Quote</th>
            <th>Shares</th>
            <th>Amount</th>
          </tr>
        </thead>
        <tbody>
          ${enrichedSecurityData
            .slice(0, 10)
            .map(d => {
              // Get the last price for this ticker
              let lastPrice = tickerPrices[d.Ticker]?.lastPrice;

              // Calculate amount and shares
              const amount = investment_amount * d.completeWeight;
              const shares = lastPrice ? amount / lastPrice : 0;

              return html`
                <tr>
                  <td>${d.Type}</td>
                  <td><strong>${d.Ticker}</strong></td>
                  <td>${d.Name}</td>
                  <td>${d.Sector}</td>
                  <td>${formatPercent(d.classWeight)}</td>
                  <td>${formatPercent(d.riskyPfWeight)}</td>
                  <td>${formatPercent(d.completeWeight)}</td>
                  <td>${lastPrice !== undefined ? 
                      (d.Type === "Risk‑Free" ? formatPercent(lastPrice/100) : formatCurrency(lastPrice, "$", 2)) 
                      : "N/A"}</td>
                  <td>${d.Type === "Risk‑Free" ? "0" : (shares > 0 ? formatNumber(shares, 2) : "N/A")}</td>
                  <td>${formatCurrency(amount)}</td>
                </tr>
              `;
            })}
        </tbody>
      </table>
    </div>`;
}
  • Security Distribution
  • Risk/Return (Equities)
  • Covariances (Equities)
  • Bond Price Sensitivity
Code
viewof equityTickersBarChart = {
  if (!enrichedSecurityData || enrichedSecurityData.length === 0) 
    return html`<div>No ticker data available</div>`;
  
  // Use the first 10 items from the already sorted enriched data
  const barData = enrichedSecurityData
    .slice(0, 10)
    .map(d => ({
      asset: d.Ticker,
      weight: d.completeWeight * 100,  // %
      type: d.Type === "Risk‑Free" ? "Risk-Free" : d.Type // Handle dash style differences
    }));
  
  // Group securities by asset type to create color gradient scales
  const groupedByType = d3.group(barData, d => d.type);
  
  // Create a color function that generates gradients for asset classes with multiple securities
  const getSecurityColor = (d) => {
    const baseColor = assetColors[d.type];
    const securities = groupedByType.get(d.type);
    
    // If there's only one security in this asset class, use the base color
    if (securities.length <= 1) return baseColor;
    
    // Otherwise create a gradient based on position in the group
    const index = securities.findIndex(s => s.asset === d.asset);
    const position = index / (securities.length - 1); // 0 to 1
    
    // Create a gradient from the base color to a lighter version
    return d3.interpolate(
      baseColor, 
      d3.color(baseColor).brighter(0.8)
    )(position);
  };

  // Bar chart
  return Plot.plot({
    marginTop: 20,
    marginLeft: 60,
    x: {
      label: "",
      domain: barData.map(d => d.asset),
      padding: 0.3
    },
    y: {
      label: "Weight (%)",
      domain: [0, Math.max(...barData.map(d => d.weight)) * 1.1],
      grid: true,
      tickFormat: d => d + "%"
    },
    marks: [
      Plot.barY(barData, {
        x: "asset",
        y: "weight",
        fill: getSecurityColor, // Use our custom gradient function
        tip: true,
        title: d => `${d.asset} (${d.type}): ${d.weight.toFixed(1)}%`
      }),
      Plot.text(barData, {
        x: "asset",
        y: "weight",
        text: d => `${d.weight.toFixed(1)}%`,
        dy: -10,
        fontWeight: "bold",
        fontSize: 16
      })
    ]
  });
}
Code
securityAllocationTable()
Code
// Set up color scale matching the 'Spectral' palette
riskReturnColorScale = d3.scaleSequential()
  .domain([d3.min(risk_return_data, d => d.Return), d3.max(risk_return_data, d => d.Return)])
  .interpolator(d3.interpolateSpectral);
Code
// Create risk-return scatter plot
viewof riskReturnPlot = {
  // Create the plot with interactive elements
  const plot = Plot.plot({
    marginBottom: 40,
    marginRight: 40,
    marginLeft: 60,
    grid: true,
    style: {
      background: "transparent",
      fontSize: "12px",
      fontFamily: "system-ui, sans-serif"
    },
    x: {
      label: "Risk (Standard Deviation)",
      tickFormat: d => d3.format(".1%")(d),
      domain: [
        d3.min(risk_return_data, d => d.Risk) * 0.95, 
        d3.max(risk_return_data, d => d.Risk) * 1.05
      ]
    },
    y: {
      label: "Expected Return (Annualized)",
      tickFormat: d => d3.format(".1%")(d),
      domain: [
        Math.min(0, d3.min(risk_return_data, d => d.Return) * 1.1),
        d3.max(risk_return_data, d => d.Return) * 1.1
      ]
    },
    marks: [
      // Horizontal line at y=0
      Plot.ruleY([0], {stroke: "gray", strokeDasharray: "4,4", strokeOpacity: 0.7}),
      
      // Regular ticker points
      Plot.dot(
        risk_return_data.filter(d => !d.isPortfolio), 
        {
          x: "Risk",
          y: "Return",
          fill: d => riskReturnColorScale(d.Return),
          stroke: "black",
          strokeWidth: 1.5,
          r: 8,
          title: d => `${d.Ticker} \nReturn: ${(d.Return * 100).toFixed(2)}% \nRisk (STDev): ${(d.Risk * 100).toFixed(2)}%`,
          tip: true
        }
      ),
      
      // Portfolio point with star marker
      Plot.dot(
        risk_return_data.filter(d => d.isPortfolio),
        {
          x: "Risk",
          y: "Return",
          fill: "black",
          stroke: "white",
          strokeWidth: 2,
          r: 12,
          symbol: "star",
          title: d => `Weighted Portfolio \nReturn: ${(d.Return * 100).toFixed(2)}% \nRisk (STDev): ${(d.Risk * 100).toFixed(2)}%`,
          tip: true
        }
      ),
      
      // Ticker labels
      Plot.text(
        risk_return_data,
        {
          x: "Risk",
          y: "Return",
          text: "Ticker",
          dy: d => d.isPortfolio ? -16 : -14,
          fontSize: d => d.isPortfolio ? 14 : 11,
          fontWeight: "bold",
          fill: d => d.isPortfolio ? "#222" : "#444",
          stroke: "white",
          strokeWidth: 2,
          paintOrder: "stroke",
          textAnchor: "middle"
        }
      ),
      
      // Interactive elements
      
      // Add vertical tracking line (x-axis)
      Plot.ruleX(risk_return_data, Plot.pointerX({
        x: "Risk",
        stroke: "#666", 
        strokeWidth: 1, 
        strokeDasharray: "4 4"
      })),
      
      // Add horizontal tracking line (y-axis)
      Plot.ruleY(risk_return_data, Plot.pointer({
        y: "Return",
        stroke: "gray", 
        strokeWidth: 1,
        strokeDasharray: "3,3"
      })),
    ]
  });
  
  // Return container with header and plot
  return html`
    <div class="chart-title">
      <h5 class="chart-title">Risk & Return Profile (Individual Equity Securities)</h5>
      ${plot}
    </div>
  `;
}
Code
html`
  <div class="chart-title">
    <h5 class="chart-title" style="margin-left: 1rem;">Efficient Frontier (Weighted Equity Securities)</h5>
    <iframe class="frontier-container" scrolling="no" style="width: 100%; height: 100%; position: relative;  overflow: clip !important; margin-block-start: -25px;" src="${EFFICIENT_FRONTIER}">
    </iframe>
  </div>`;
// makeFullscreen(EFFICIENT_FRONTIER)
Optimal Equity Portfolio Weights
Ticker PGR GE WMT TMUS AAPL
Weight 31.16% 28.79% 21.59% 10.11% 8.35%
Expected Return 24.74% 36.14% 16.42% 17.58% 19.61%
Standard Deviation 25.18% 35.19% 21.16% 24.95% 29.82%
Correlation Matrix
PGR GE WMT TMUS AAPL
PGR 1.00 0.23 0.23 0.30 0.20
GE 0.23 1.00 0.16 0.21 0.27
WMT 0.23 0.16 1.00 0.24 0.31
TMUS 0.30 0.21 0.24 1.00 0.33
AAPL 0.20 0.27 0.31 0.33 1.00
Standard Deviations (Annualized)
Standard Deviation
PGR 25.06%
GE 35.27%
WMT 21.09%
TMUS 24.95%
AAPL 29.91%
Covariance Matrix (Annualized)
PGR GE WMT TMUS AAPL
PGR 0.062821 0.020164 0.012148 0.018891 0.015006
GE 0.020164 0.124373 0.011879 0.018899 0.028864
WMT 0.012148 0.011879 0.044487 0.012621 0.019250
TMUS 0.018891 0.018899 0.012621 0.062243 0.024294
AAPL 0.015006 0.028864 0.019250 0.024294 0.089457
Security Weights
Weight
PGR 31.16%
GE 28.79%
WMT 21.59%
TMUS 10.11%
AAPL 8.35%
Covariance Matrix (Weighted)
PGR GE WMT TMUS AAPL
PGR 0.006098 0.001809 0.000817 0.000595 0.000390
GE 0.001809 0.010312 0.000738 0.000550 0.000694
WMT 0.000817 0.000738 0.002073 0.000276 0.000347
TMUS 0.000595 0.000550 0.000276 0.000637 0.000205
AAPL 0.000390 0.000694 0.000347 0.000205 0.000624
Code
viewof bondSensitivityChart = {
  // Generate YTM changes from -1.25% to 1.25% in 0.25% increments
  const ytmChanges = d3.range(-125, 150, 25).map(d => d / 10000);
  
  // Prepare bond data array
  const bondData = [];
  const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
  
  // Process bond_fundamentals data
  if (bond_fundamentals) {
    // Handle the object-of-arrays structure of bond_fundamentals
    const numBonds = bond_fundamentals.Name ? bond_fundamentals.Name.length : 0;
    
    if (numBonds > 0) {
      // For each bond index
      for (let i = 0; i < numBonds; i++) {
        // Extract Ticker (or create one if missing)
        const ticker = Array.isArray(bond_fundamentals.Ticker) && i < bond_fundamentals.Ticker.length 
          ? bond_fundamentals.Ticker[i]
          : `Bond ${i+1}`;
          
        // Extract Name
        const name = Array.isArray(bond_fundamentals.Name) && i < bond_fundamentals.Name.length
          ? bond_fundamentals.Name[i]
          : "";
          
        // Parse duration and convexity values
        const duration = Array.isArray(bond_fundamentals["Duration (D*)"]) && i < bond_fundamentals["Duration (D*)"].length
          ? parseFloat(bond_fundamentals["Duration (D*)"][i]) || 0
          : 0;
          
        const convexity = Array.isArray(bond_fundamentals.Convexity) && i < bond_fundamentals.Convexity.length
          ? parseFloat(bond_fundamentals.Convexity[i]) || 0
          : 0;
        
        // Skip bonds with missing duration or convexity
        if (isNaN(duration) || isNaN(convexity) || duration === 0 || convexity === 0) continue;
        
        // Add to our data array
        bondData.push({
          ticker: ticker,
          name: name,
          duration: duration,
          convexity: convexity,
          color: colorScale(i)
        });
      }
    }
  }
  
  // If no valid bond data is available, return a message
  if (bondData.length === 0) {
    return html`<div>
      <h5 class="table-title">Bond Price Sensitivity to Changes in YTM</h5>
      <div class="alert alert-warning">
        No bond data with valid duration and convexity values available for sensitivity analysis.
        <pre style="font-size:10px; max-height: 100px; overflow: auto;">
          ${JSON.stringify(bond_fundamentals, null, 2)}
        </pre>
      </div>
    </div>`;
  }
  
  // Rest of the function remains the same...
  const chartData = [];
  bondData.forEach(bond => {
    ytmChanges.forEach(ytmChange => {
      // Calculate price change using both duration and convexity
      const priceChange = -bond.duration * ytmChange + 0.5 * bond.convexity * ytmChange * ytmChange;
      
      chartData.push({
        ticker: bond.ticker,
        name: bond.name,
        ytmChange: ytmChange * 100, // Convert to percentage for display
        priceChange: priceChange * 100, // Convert to percentage for display
        duration: bond.duration, // Add duration and convexity for tooltips
        convexity: bond.convexity,
        color: bond.color
      });
    });
  });
  
  // Create the plot element
  const plot = Plot.plot({
    marginBottom: 40,
    marginRight: 40,
    marginLeft: 60,
    x: {
      label: "Change in YTM (%)",
      tickFormat: d => d.toFixed(2) + "%",
      domain: [-1.25, 1.25]
    },
    y: {
      label: "Price Change (%)",
      tickFormat: d => d.toFixed(2) + "%",
      // Calculate domain dynamically from data with 10% padding
      domain: [
        d3.min(chartData, d => d.priceChange) * 1.1, 
        d3.max(chartData, d => d.priceChange) * 1.1
      ]
    },
    grid: true,
    marks: [
      // Zero lines
      Plot.ruleY([0], {stroke: "#ccc", strokeWidth: 1}),
      Plot.ruleX([0], {stroke: "#ccc", strokeWidth: 1}),
      
      // Bond price change lines (without tooltip)
      Plot.line(chartData, {
        x: "ytmChange",
        y: "priceChange",
        stroke: d => d.color,
        strokeWidth: 2.5,
        curve: "linear"
      }),
      
      // Add separate tooltip mark
      Plot.tip(chartData, Plot.pointer({
        strokeWidth: 2,
        fontSize: 14,
        curve: "linear",
        x: "ytmChange",
        y: "priceChange",
        title: d => [
          `${d.ticker}`,
          `${d.name}`, 
          `Change in YTM: ${d.ytmChange.toFixed(2)}%`,
          `Price Change: ${d.priceChange.toFixed(2)}%`,
          `Duration (D*): ${d.duration.toFixed(2)}`,
          `Convexity: ${d.convexity.toFixed(2)}`
        ].join("\n")
      })),
      
      // Bond ticker labels at the right end of each line
      Plot.text(d3.groups(chartData, d => d.ticker).map(([ticker, values]) => {
        const lastPoint = values.reduce((max, v) => v.ytmChange > max.ytmChange ? v : max, values[0]);
        return {
          ticker,
          name: lastPoint.name,
          ytmChange: lastPoint.ytmChange,
          priceChange: lastPoint.priceChange,
          color: lastPoint.color
        };
      }), {
        x: "ytmChange",
        y: "priceChange",
        text: "ticker",
        dx: 5,
        fill: d => d.color,
        fontWeight: "bold",
        fontSize: 12
      }),
      
      // Chart title
      Plot.text([{x: 0, y: 0, text: "Change in YTM vs. Change in Price"}], {
        frameAnchor: "top",
        dy: -25,
        fontSize: 16,
        fontWeight: "bold"
      }),
      // Add hover line like in the ticker chart
      Plot.ruleX(chartData, Plot.pointerX({
        x: "ytmChange",
        stroke: "#666", 
        strokeWidth: 1, 
        strokeDasharray: "4 4"
      }))
    ],
    style: {
      fontFamily: "system-ui, sans-serif",
      background: "transparent"
    }
  });
  
  // Return container with header and plot
  return html`
    <div class="chart-title">
      <h5 class="chart-title">Bond Price Sensitivity to Changes in YTM (Yield to Maturity)</h5>
      ${plot}
    </div>
  `;
}
Code
function bondFundamentalsTable() {
  if (!bond_fundamentals || !bond_fundamentals.Ticker || bond_fundamentals.Ticker.length === 0)
    return html`<div>No bond data available</div>`;
  
  // Create arrays of data from columns
  const numBonds = bond_fundamentals.Ticker.length;
  const tableData = [];
  
  for (let i = 0; i < numBonds; i++) {
    // Only add rows with valid data
    if (bond_fundamentals.Ticker[i]) {
      tableData.push({
        ticker: bond_fundamentals.Ticker[i],
        name: bond_fundamentals.Name ? bond_fundamentals.Name[i] : "N/A",
        ytm: bond_fundamentals.Yield_To_Maturity ? bond_fundamentals.Yield_To_Maturity[i] : "N/A",
        duration: bond_fundamentals["Duration (D*)"] ? bond_fundamentals["Duration (D*)"][i] : "N/A",
        convexity: bond_fundamentals.Convexity ? bond_fundamentals.Convexity[i] : "N/A",
        price: bond_fundamentals.Bond_Price ? bond_fundamentals.Bond_Price[i] : "N/A",
        coupon: bond_fundamentals.Weighted_Avg_Coupon ? bond_fundamentals.Weighted_Avg_Coupon[i] : "N/A",
        maturity: bond_fundamentals.Weighted_Avg_Maturity ? bond_fundamentals.Weighted_Avg_Maturity[i] : "N/A",
        sensitivity: bond_fundamentals["Price Sensitivity to YTM (-1%)"] ? 
                     bond_fundamentals["Price Sensitivity to YTM (-1%)"][i] : "N/A",
        stdDev: bond_fundamentals.Standard_Deviation ? bond_fundamentals.Standard_Deviation[i] : "N/A"
      });
    }
  }
  
  // Sort the table data by price sensitivity in descending order
  tableData.sort((a, b) => {
    // Convert to numbers for comparison
    const sensA = parseFloat(a.sensitivity) || 0;
    const sensB = parseFloat(b.sensitivity) || 0;
    
    // Sort in descending order (b - a)
    return sensB - sensA;
  });

  return html`
    <div style="overflow-x: auto;">
    <h5 class="table-title">Price Change to a <u>Decrease</u> in YTM (-1%)</h5>
      <table class="table-responsive">
        <thead>
          <tr>
            <th>Ticker</th>
            <th>Name</th>
            <th>Current Price (PV)</th>
            <th>New Price</th>
            <th>Current YTM</th>
            <th>New YTM</th>
            <th>Coupon Rate (%)</th>
            <th>Maturity (yrs)</th>
            <th>Duration (D*)</th>
            <th>Convexity</th>
            <th>Price Change (%)</th>
          </tr>
        </thead>
        <tbody>
          ${tableData.map(bond => {
            // Parse numerical values
            const currentYtm = typeof bond.ytm === 'string' ? parseFloat(bond.ytm) : bond.ytm;
            const currentPrice = typeof bond.price === 'string' ? parseFloat(bond.price) : bond.price;
            const duration = typeof bond.duration === 'string' ? parseFloat(bond.duration) : bond.duration;
            const convexity = typeof bond.convexity === 'string' ? parseFloat(bond.convexity) : bond.convexity;
            
            // Calculate new YTM (current - 1%)
            const newYtm = !isNaN(currentYtm) ? Math.max(0, currentYtm - 0.01) : null;
            
            // Calculate new price using duration and convexity formula
            // ΔP/P = -D* × Δy + 1/2 × C × (Δy)²
            const ytmChange = -0.01; // Decrease of 1%
            const priceChangePercent = !isNaN(duration) && !isNaN(convexity) ? 
              (-duration * ytmChange + 0.5 * convexity * Math.pow(ytmChange, 2)) : null;
            
            const newPrice = !isNaN(currentPrice) && priceChangePercent !== null ?
              currentPrice * (1 + priceChangePercent) : null;
            
            // Format values appropriately
            const ytmDisplay = typeof bond.ytm === 'string' ? bond.ytm : 
                              (bond.ytm ? formatPercent(parseFloat(bond.ytm)) : "N/A");
            
            const newYtmDisplay = newYtm !== null ? formatPercent(newYtm) : "N/A";
            
            const couponDisplay = typeof bond.coupon === 'string' ? bond.coupon : 
                                 (bond.coupon ? formatPercent(parseFloat(bond.coupon)) : "N/A");
            
            const priceDisplay = typeof bond.price === 'string' ? bond.price : 
                               (bond.price ? formatCurrency(parseFloat(bond.price)) : "N/A");
            
            const newPriceDisplay = newPrice !== null ? formatCurrency(newPrice) : "N/A";
            
            const sensitivityDisplay = typeof bond.sensitivity === 'string' ? bond.sensitivity : 
                                     (bond.sensitivity ? formatPercent(parseFloat(bond.sensitivity)) : "N/A");
                                
            return html`
              <tr>
                <td><strong>${bond.ticker}</strong></td>
                <td>${bond.name}</td>
                <td>${priceDisplay}</td>
                <td>${newPriceDisplay}</td>
                <td>${ytmDisplay}</td>
                <td>${newYtmDisplay}</td>
                <td>${couponDisplay}</td>
                <td>${bond.maturity}</td>
                <td>${bond.duration}</td>
                <td>${bond.convexity}</td>
                <td>${sensitivityDisplay}</td>
              </tr>
            `;
          })}
        </tbody>
      </table>
    </div>
  `;
}

// Display the table
bondFundamentalsTable()
  • Security Quotes
  • Stock Fundamentals
  • Bond Fundamentals
  • Daily
  • Monthly
Adjusted Closing Price (5Y Daily)
Date GE PGR WMT AAPL TMUS ^IRX TLT ADME
Loading ITables v2.3.0 from the internet... (need help?)
Adjusted Closing Price (5Y Monthly)
Date GE PGR WMT AAPL TMUS ^IRX TLT ADME
Loading ITables v2.3.0 from the internet... (need help?)
Ticker AAPL GE PGR TMUS WMT
Name Apple Inc. GE Aerospace The Progressive Corporation T-Mobile US, Inc. Walmart Inc.
Date 2025-05-19 2025-05-19 2025-05-19 2025-05-19 2025-05-19
Sector Technology Industrials Financial Services Communication Services Consumer Defensive
Industry Consumer Electronics Aerospace & Defense Insurance - Property & Casualty Telecom Services Discount Stores
Country United States United States United States United States United States
Website https://www.apple.com https://www.geaerospace.com https://www.progressive.com https://www.t-mobile.com https://corporate.walmart.com
Market Cap 3,116,055,920,640.00 250,158,039,040.00 169,310,289,920.00 278,633,742,336.00 785,687,379,968.00
Enterprise Value 3,205,030,477,824.00 255,093,096,448.00 171,781,554,176.00 385,216,118,784.00 851,749,634,048.00
Float Shares 14,911,480,604.00 1,062,600,970.00 584,025,304.00 1,041,100,974.00 4,346,047,610.00
Shares Outstanding 14,935,799,808.00 1,066,390,016.00 586,224,000.00 1,135,449,984.00 8,000,889,856.00
P/E (trailing) 32.50 36.94 19.49 23.96 41.97
P/E (forward) 25.11 44.77 20.93 23.00 36.10
P/S 7.78 6.30 2.16 3.37 1.15
P/B 46.66 12.99 5.85 4.57 9.39
EV/EBITDA 23.08 26.13 14.84 12.19 20.04
EV/Revenue 8.01 6.43 2.19 4.66 1.24
Gross Margin (%) 46.63% 31.88% 14.98% 63.85% 24.88%
EBITDA Margin (%) 34.68% 24.60% 14.74% 38.22% 6.20%
Operating Margin (%) 31.03% 21.98% 16.20% 22.98% 4.31%
Profit Margin (%) 24.30% 17.63% 11.10% 14.41% 2.75%
ROE 1.38 0.27 0.34 0.19 0.22
ROA 0.24 0.04 0.07 0.06 0.07
Revenue (TTM) 400,366,010,368.00 39,680,999,424.00 78,507,999,232.00 82,691,997,696.00 685,086,015,488.00
Revenue Growth (%) 5.10% 10.90% 18.40% 6.60% 2.50%
EPS (trailing) 6.42 6.35 14.82 10.24 2.34
EPS (forward) 8.31 5.24 13.80 10.67 2.72
Earnings Growth (%) 7.80% 31.40% 10.90% 29.00% -11.10%
Earnings Quarterly Growth (%) 4.80% 28.50% 10.10% 24.40% -12.10%
Total Cash 48,497,999,872.00 13,004,999,680.00 2,790,000,128.00 12,003,000,320.00 9,310,999,552.00
Total Debt 98,186,002,432.00 20,714,000,384.00 6,894,000,128.00 121,691,996,160.00 67,205,001,216.00
Debt to Equity 146.99 106.4 23.81 199.15 74.14
Current Ratio 0.82 1.08 0.33 1.16 0.78
Quick Ratio 0.68 0.73 0.26 0.9 0.18
Book Value 4.47 18.05 49.41 53.73 10.46
Free Cash Flow 97,251,500,032.00 2,622,874,880.00 13,350,724,608.00 9,022,624,768.00 9,801,124,864.00
Operating Cash Flow 109,555,998,720.00 5,224,999,936.00 16,026,999,808.00 24,056,000,512.00 37,604,999,168.00
Dividend Yield (%) 0.49% 0.62% 1.71% 1.45% 0.96%
Dividend Rate (%) 1.04% 1.44% 4.90% 3.52% 0.94%
5Y Avg Dividend Yield (%) 0.57% 0.40% 1.58% 0.00% 1.43%
Payout Ratio (%) 15.58% 23.34% 33.04% 29.88% 36.65%
Price 208.63 234.58 288.82 245.4 98.2
Target Price 229.61 226.9 294.88 269.57 107.88
Target High 300 261 330 305 120
Target Low 170.62 196.11 183.0 202.99 64.0
Analyst Rating buy strong_buy buy buy buy
Analyst Rating Value 2.09 1.4 2.25 2.03 1.51
Beta 1.27 1.31 0.4 0.68 0.7
52W High 260.1 234.67 292.99 276.49 105.3
52W Low 169.21 150.2 201.34 163.15 63.87
50 Day Avg 208.47 198.98 276.07 254.46 91.25
200 Day Avg 226.24 185.9 256.33 230.63 87.55
Short Ratio 1.43 1.56 1.7 2.29 1.95
Short % of Float 0.01 0.01 0.01 0.03 0.01
Ticker TLT ILTB IGLB TLH LQDI LQD IGOV ICVT
Name iShares 20+ Year Treasury Bond ETF iShares Core 10+ Year USD Bond ETF iShares 10+ Year Investment Grade Corporate Bo... iShares 10-20 Year Treasury Bond ETF iShares Inflation Hedged Corporate Bond ETF iShares iBoxx $ Investment Grade Corporate Bon... iShares International Treasury Bond ETF iShares Convertible Bond ETF
Perf. as of Apr 30, 2025 Apr 30, 2025 Apr 30, 2025 Apr 30, 2025 Apr 30, 2025 Apr 30, 2025 Apr 30, 2025 Apr 30, 2025
Inception Date Jul 22, 2002 Dec 08, 2009 Dec 08, 2009 Jan 05, 2007 May 08, 2018 Jul 22, 2002 Jan 21, 2009 Jun 02, 2015
Net Assets 48,796,891,937.00 591,313,158.00 2,498,390,313.00 9,898,819,870.00 90,392,195.00 29,697,566,053.00 976,165,647.00 2,472,301,830.00
Bond_Price 69.698 78.632 83.077 80.199 52.456 90.748 94.584 97.769
Price Sensitivity to YTM (-1%) 17.86% 14.89% 14.58% 12.87% 11.69% 9.74% 8.53% 2.41%
Standard_Deviation 16.30% 12.85% 12.74% 12.88% 8.89% 8.82% 9.54% 15.14%
Duration (D*) 17.861 14.885 14.584 12.869 11.693 9.74 8.531 2.413
Convexity 3.38 2.34 2.17 1.88 1.07 1.11 1.11 1.2
Weighted_Avg_Maturity 25.48 21.76 22.1 16.96 12.45 12.82 9.59 2.93
Weighted_Avg_Coupon 2.90% 3.90% 4.60% 3.20% 0.00% 4.40% 2.20% 2.10%
Yield_To_Maturity 5.00% 5.70% 6.00% 4.90% 5.20% 5.40% 2.90% 2.90%
YTD (%) 3.23% 2.20% 1.10% 4.02% 2.32% 2.22% 8.65% 0.45%
1Y (%) 5.46% 6.73% 6.19% 7.73% 6.38% 7.50% 9.56% 13.09%
3Y (%) -6.03% -1.03% 0.87% -2.63% 2.12% 2.61% -0.71% 5.30%
5Y (%) -9.43% -3.92% -1.95% -6.71% 4.01% -0.17% -3.32% 10.75%
10Y (%) -0.87% 1.47% 2.09% -0.35% 0 2.37% -0.86% 0
Incept (%) 3.79% 3.87% 4.27% 2.95% 3.95% 4.41% 0.21% 9.08%
Product_ID 239454 239424 239423 239453 294319 239566 239830 272819
URL https://www.blackrock.com/us/individual/produc... https://www.blackrock.com/us/individual/produc... https://www.blackrock.com/us/individual/produc... https://www.blackrock.com/us/individual/produc... https://www.blackrock.com/us/individual/produc... https://www.blackrock.com/us/individual/produc... https://www.blackrock.com/us/individual/produc... https://www.blackrock.com/us/individual/produc...
Code
ExcelModel = html`
  <div>
    <div style="width: 100%; height: "100%"; position: absolute;">
    <iframe height="100%" width="100%" 
        id="benchmarkModel-embed" 
        title="benchmarkModel Embed"
        src="https://1drv.ms/x/c/bde1a904e346bc6a/IQRTEwB280IjSazAjML3PbuuAd4_2bk5zHNh5guP6706TTo?em=2&AllowTyping=True&AllowFormulaEntry=True&ActiveCell='Cover'!A1&wdHideGridlines=True&wdInConfigurator=True&wdShowFormulaBar=True&wdInConfigurator=True"
        frameborder="0" allow="clipboard-write" allowfullscreen 
        style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;"></iframe>
    </div>
  </div>
`;
Code
oneDrivePath = "https://1drv.ms/b/c/bde1a904e346bc6a/IQQuqGRKnWXbQboG_DKlVFQ2AWvJYsVGi_bWvHRqpK4ETAs";
embedUrl = `${oneDrivePath}?embed=true`;

html`
  <div style="display: flex; flex-direction: column; height: 100%;">
    <div style="position: absolute; top: 0; left: 0; width: 100%; height: 97%;">
      <iframe 
        width="100%" 
        height="100%" 
        src="${embedUrl}"
        frameborder="0"
        allowfullscreen
      ></iframe>
    </div>
  </div>
`;
Market Info:
Code
viewof rf_rate = createPercentInput({
  min: 0.01, 
  max: 0.1, 
  value: default_rf_rate, 
  step: 0.001, 
  label: "Risk-Free Rate (T-Bill):"
})

md`**Risk-Free Rate: ${formatPercent(rf_rate)}**`
Code
// Use imported function to create market condition input
viewof raw_market_view = createNumericInput({
  min: 0,
  max: 100,
  value: default_market_view,
  step: 5,
  label: "Market Condition:",
  decimals: 0
})
Code
market_view = raw_market_view === 0 ? 1 : raw_market_view

// Use categorizeValue function to format market condition
market_condition = categorizeValue(
  market_view, 
  [25, 50, 75, 100], 
  ["Bear", "Normal", "Bull", "Bubble"]
);
Code
md`**Market Condition: ${market_condition}**`
Code
md`
- Bear Market: 1-25
- Normal Market: 25-50  
- Bull Market: 50-75
- Bubble Market: 75-100
`
Client Info:
Code
// Create risk tolerance input using our component
viewof raw_risk_score = createNumericInput({
  min: 0,
  max: 100,
  value: default_risk_score,
  step: 5,
  label: "Risk Tolerance:",
  decimals: 0
})
Code
risk_score = raw_risk_score === 100 ? 99 : raw_risk_score

// Use categorizeValue function for risk tolerance display
risk_tolerance_score = categorizeValue(
  risk_score,
  [40, 70, 100],
  ["Conservative", "Moderate", "Aggressive"]
);

// Calculate risk aversion index
risk_aversion = (market_view * (1 - risk_score/100)).toFixed(2);
Code
md`**Risk Tolerance: ${risk_tolerance_score}**`
Code
md`
- Conservative: 0-40
- Moderate: 40-70  
- Aggressive: 70-100
`
Code
// Display risk aversion index
md`**Risk Aversion Index: ${risk_aversion}**
`
Code
// Create Investment Amount with currency formatting
viewof investment_amount = createCurrencyInput({
  min: 10000,
  max: 1000000,
  value: 100000,
  step: 5000,
  label: "Investment Amount:",
  decimals: 0
})
Code
// Create Investment Time Horizon
viewof time_horizon = createNumericInput({
  min: 1,
  max: 50,
  value: 5,
  step: 1,
  label: "Time Horizon (Years):",
  format: "integer",
  decimals: 0
})
Risky Portfolio:
Code
// Use the imported findOptimalEquityWeight function
optimal_equity_weight = findOptimalEquityWeight({
  equity_return: calc_equity_return || 0.237,
  equity_std: calc_equity_std || 0.161,
  bond_return: bond_ret,
  bond_std: bond_vol, 
  correlation: corr_eq_bd,
  rf_rate: rf_rate
});
Code
// Use the imported asset allocation slider
viewof equity_weight = createAssetAllocationSlider({
  value: optimal_equity_weight,
  asset1Name: "Bonds",
  asset2Name: "Equity",
  asset1Color: "#1c7ed6",
  asset2Color: "#FF7F50",
  onOptimize: () => findOptimalEquityWeight({
    equity_return: calc_equity_return || 0.237,
    equity_std: calc_equity_std || 0.161,
    bond_return: bond_ret,
    bond_std: bond_vol, 
    correlation: corr_eq_bd,
    rf_rate: rf_rate
  })
})
Code
viewof fixed_optimal_weight_input = {
  const container = html`<div style="display: none;"></div>`;
  
  // Initialize with null (use calculated value initially)
  container.value = null;
  
  return container;
}

// Use calculateRiskyPortfolioMetrics instead of manual calculations
portfolio_metrics = calculateRiskyPortfolioMetrics(
  equity_weight,
  calc_equity_return || 0.237,
  calc_equity_std || 0.161,
  bond_ret,
  bond_vol,
  corr_eq_bd
);

// Extract values from the result
er_risky = portfolio_metrics.er_risky;
std_dev_risky = portfolio_metrics.std_dev_risky;
bond_weight = portfolio_metrics.bond_weight;

// Get data based on current inputs
data = calculateData(
  rf_rate,
  er_risky, 
  std_dev_risky,
  market_view,
  risk_score
);

// Extract data properties for use in visualizations
allocation_data = data.allocation_data;
chart_data = data.chart_data;
optimal_weight = data.optimal_weight;
risk_aversion_weight = data.risk_aversion_weight;
er_optimal = data.er_optimal;
std_dev_optimal = data.std_dev_optimal;
utility_optimal = data.utility_optimal;
sharpe_ratio = data.sharpe_ratio;
max_utility_idx = data.max_utility_idx;

// Calculate fixed data for the default values
fixed_data = calculateData(
  rf_rate,
  default_er_risky,
  default_std_dev_risky,
  market_view,
  risk_score
);

// Extract fixed data properties
fixed_chart_data = fixed_data.chart_data;
fixed_allocation_data = fixed_data.allocation_data;
fixed_max_utility_idx = fixed_data.max_utility_idx;
calculated_optimal_weight = fixed_data.optimal_weight;
fixed_er_optimal = fixed_data.er_optimal;
fixed_std_dev_optimal = fixed_data.std_dev_optimal;
fixed_utility_optimal = fixed_data.utility_optimal;

// Use either the user-selected weight from table click or the calculated optimal weight
fixed_optimal_weight = fixed_optimal_weight_input !== null ? 
  fixed_optimal_weight_input : calculated_optimal_weight;

// Reset manual selection when key inputs change
resetManualSelectionWatcher = {
  // Create a composite key from the inputs we want to watch
  const inputKey = `${rf_rate}-${market_view}-${risk_score}`;
  
  // Store this value for comparison
  if (this.lastInputKey !== undefined && 
      this.lastInputKey !== inputKey && 
      fixed_optimal_weight_input !== null) {
    
    // Reset the fixed_optimal_weight_input to null
    // This will cause fixed_optimal_weight to use calculated_optimal_weight
    viewof fixed_optimal_weight_input.value = null;
    viewof fixed_optimal_weight_input.dispatchEvent(new Event("input"));
    
    // Remove highlighting from selected row (if we can access the DOM)
    if (typeof document !== 'undefined') {
      setTimeout(() => {
        const selectedRows = document.querySelectorAll('.table-responsive .selected-row');
        selectedRows.forEach(row => row.classList.remove('selected-row'));
      }, 0);
    }
  }
  
  // Update last value for next comparison
  this.lastInputKey = inputKey;
  
  return null; // This cell doesn't need to return a value
}

// Initialize base data reference
initialData = initial_data;
Code
// Display risky portfolio stats using formatting functions
md`
**Sharpe Ratio: ${formatNumber((er_risky - rf_rate) / std_dev_risky, 2)}**
- Expected Return: ${formatPercent(er_risky)}
- Standard Deviation: ${formatPercent(std_dev_risky)}
`
Complete Portfolio:
Code
// Show calculated portfolio statistics with consistent formatting
md`
**Sharpe Ratio: ${formatNumber(sharpe_ratio, 2)}**
- Expected Return: ${formatPercent(er_optimal)}
- Standard Deviation: ${formatPercent(std_dev_optimal)}

**Weights:**
- Risk-Free Asset: ${formatPercent(1 - fixed_optimal_weight)}
- Risky Portfolio: ${formatPercent(fixed_optimal_weight)}
  - Equity: ${formatPercent(fixed_optimal_weight * equity_weight)} (${formatPercent(equity_weight, 0)} of Risky)
  - Bonds: ${formatPercent(fixed_optimal_weight * bond_weight)} (${formatPercent(bond_weight, 0)} of Risky)
`