- Descripción General
- Arquitectura del Sistema
- Interfaz de Usuario
- Funcionalidades Principales
- Métricas y Cálculos
- Almacenamiento de Datos
- Flujo de Trabajo
- API y Módulos
El Dashboard de Portfolio Tracking es un sistema completo de seguimiento y análisis de carteras de inversión que permite a los usuarios:
- Guardar y gestionar múltiples portfolios
- Visualizar en tiempo real el valor y P&L de sus inversiones
- Analizar el rendimiento con métricas cuantitativas profesionales
- Comparar el rendimiento contra benchmarks de mercado
- Recibir alertas automáticas sobre riesgos y desviaciones
- Consultar histórico completo de rebalanceos y cambios
✅ 100% Client-Side: No requiere backend, usa IndexedDB para persistencia local ✅ Tiempo Real: Actualización automática de precios vía API de Yahoo Finance ✅ Profesional: Métricas de nivel institucional (Sharpe, Sortino, VaR, CVaR) ✅ Visual: Gráficos interactivos con Chart.js ✅ Multiidioma: Soporte completo para Español e Inglés ✅ Responsive: Diseño adaptable a diferentes pantallas
┌─────────────────────────────────────────────────────────────┐
│ INTERFAZ DE USUARIO │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Selector │ │ Cards de │ │ Tabs de Vistas │ │
│ │ Portfolio │ │ Métricas │ │ (4 gráficos) │ │
│ └─────────────┘ └──────────────┘ └──────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Tabla de Posiciones con P&L │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────┐ ┌────────────────────────────────────┐ │
│ │ Métricas │ │ Histórico de Rebalanceos │ │
│ │ de Riesgo │ │ y Sistema de Alertas │ │
│ └─────────────┘ └────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ CAPA DE CONTROLADORES │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ portfolio-dashboard.js │ │
│ │ • Gestión de estado │ │
│ │ • Renderizado de UI │ │
│ │ • Manejo de eventos │ │
│ │ • Integración con Chart.js │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ CAPA DE LÓGICA DE NEGOCIO │
│ ┌──────────────────────┐ ┌─────────────────────────────┐ │
│ │ PortfolioManager │ │ PerformanceTracker │ │
│ │ • CRUD portfolios │ │ • Cálculo de P&L │ │
│ │ • Snapshots │ │ • Equity curve │ │
│ │ • Rebalanceos │ │ • Drawdowns │ │
│ │ • Posiciones │ │ • Métricas de riesgo │ │
│ │ │ │ • Comparación benchmark │ │
│ └──────────────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ CAPA DE PERSISTENCIA │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ IndexedDBStore │ │
│ │ • portfolios (Object Store) │ │
│ │ • snapshots (Object Store) │ │
│ │ • rebalances (Object Store) │ │
│ │ • price_cache (Object Store) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ FUENTES DE DATOS │
│ ┌──────────────────────┐ ┌─────────────────────────────┐ │
│ │ Yahoo Finance API │ │ Risk Engine (local) │ │
│ │ • Precios actuales │ │ • Cálculos de correlación │ │
│ │ • Datos históricos │ │ • Matrices de covarianza │ │
│ │ • Benchmarks │ │ • VaR / CVaR │ │
│ └──────────────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| Módulo | Archivo | Responsabilidad |
|---|---|---|
| UI Dashboard | src/dashboard/portfolio-dashboard.js |
Interfaz y visualización |
| Portfolio Manager | src/portfolio/portfolio-manager.js |
Gestión de portfolios |
| Performance Tracker | src/portfolio/performance-tracker.js |
Cálculos de rendimiento |
| Storage Layer | src/storage/indexed-db-store.js |
Persistencia de datos |
| Risk Engine | src/analytics/risk_engine.js |
Métricas de riesgo |
┌──────────────────────────────────────────────────────────┐
│ Seleccionar Portfolio: [Dropdown ▼] │
│ │
│ [💾 Guardar Portfolio] [🗑️ Eliminar] [🔄 Actualizar] │
└──────────────────────────────────────────────────────────┘
Elementos:
- Dropdown de Portfolios: Lista de todos los portfolios guardados
- Botón Guardar: Guarda el portfolio actual desde el constructor
- Botón Eliminar: Elimina el portfolio seleccionado (con confirmación)
- Botón Actualizar: Refresca los datos y precios actuales
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ Valor Total │ Retorno │ Sharpe Ratio │ Max Drawdown │
│ $10,542 │ +5.42% │ 1.85 │ -8.32% │
└──────────────┴──────────────┴──────────────┴──────────────┘
┌──────────────┬──────────────┐
│ Volatilidad │ Beta │
│ 15.32% │ 0.92 │
└──────────────┴──────────────┘
Colores Dinámicos:
- 🟢 Verde: Valores positivos (retornos, ratios buenos)
- 🔴 Rojo: Valores negativos (pérdidas, drawdowns)
- ⚪ Blanco: Valores neutros (volatilidad, beta)
┌─────────────────────────────────────────────────────────┐
│ [Equity Curve] [Drawdown] [vs Benchmark] [Asignación] │
├─────────────────────────────────────────────────────────┤
│ │
│ [Gráfico interactivo Chart.js] │
│ │
│ (400px altura mínima) │
│ │
└─────────────────────────────────────────────────────────┘
- Tipo: Line chart
- Eje X: Fechas (formato: YYYY-MM-DD)
- Eje Y: Valor en dólares ($)
- Características:
- Relleno degradado bajo la línea
- Tooltips interactivos
- Zoom y pan habilitados
- Color: Verde (#10b981)
- Tipo: Line chart (área negativa)
- Eje X: Fechas
- Eje Y: Porcentaje de drawdown (%)
- Características:
- Muestra la distancia del pico máximo
- Identifica periodos de pérdida
- Color: Rojo (#ef4444)
- Tipo: Multi-line chart
- Eje X: Fechas
- Eje Y: Retorno porcentual (%)
- Líneas:
- Portfolio (verde)
- Benchmark (azul)
- Información adicional:
- Alpha y Beta mostrados
- Excess return calculado
- Tipo: Doughnut chart
- Datos: Peso de cada posición (%)
- Características:
- Colores distintivos por activo
- Tooltips con porcentajes exactos
- Leyenda lateral
┌──────────────────────────────────────────────────────────────────────────┐
│ Ticker │ Nombre │ Cant │ P.Entrada│ P.Actual │ Valor │ Peso │ P&L │ P&L%│
├────────┼─────────┼──────┼──────────┼──────────┼────────┼──────┼─────┼─────┤
│ AAPL │ Apple │ 50 │ $150.00 │ $165.50 │ $8,275 │ 32.1%│ +$775│+10.3%│
│ MSFT │Microsoft│ 30 │ $320.00 │ $310.25 │ $9,307 │ 36.1%│ -$292│ -3.0%│
│ GOOGL │Alphabet │ 25 │ $125.00 │ $135.80 │ $3,395 │ 13.2%│ +$270│ +8.6%│
│ TSLA │ Tesla │ 40 │ $200.00 │ $195.50 │ $7,820 │ 30.4%│ -$180│ -2.2%│
└────────┴─────────┴──────┴──────────┴──────────┴────────┴──────┴─────┴─────┘
Columnas:
- Ticker: Símbolo del activo (color azul)
- Nombre: Nombre completo de la empresa
- Cantidad: Número de acciones
- Precio Entrada: Precio de compra promedio
- Precio Actual: Último precio conocido
- Valor: Valor total de la posición (cantidad × precio actual)
- Peso %: Porcentaje del portfolio
- P&L: Ganancia/pérdida en dólares (color según signo)
- P&L %: Ganancia/pérdida porcentual (color según signo)
┌─────────────────────────────────────────────────────────┐
│ ⚠️ Métricas de Riesgo Detalladas │
├──────────────┬──────────────┬──────────────┬───────────┤
│ VaR (95%) │ CVaR │ Sortino │ Calmar │
│ -2.35% │ -3.12% │ 1.92 │ 1.45 │
│ 1 día │ Expected │ Downside │ Ret/MaxDD │
│ │ Shortfall │ adjusted │ │
└──────────────┴──────────────┴──────────────┴───────────┘
Métricas explicadas:
- VaR (Value at Risk): Pérdida máxima esperada con 95% de confianza en 1 día
- CVaR (Conditional VaR): Pérdida promedio en el peor 5% de casos
- Sortino Ratio: Similar a Sharpe pero solo penaliza volatilidad negativa
- Calmar Ratio: Retorno anualizado dividido por max drawdown
┌─────────────────────────────────────────────────────────┐
│ 🔄 Histórico de Rebalanceos │
├─────────────────────────────────────────────────────────┤
│ 📅 2025-01-04 15:30:00 5 cambios │
│ Motivo: Drift superior al 5% desde último rebalanceo │
│ │
│ 📅 2024-12-15 10:15:00 3 cambios │
│ Motivo: Rebalanceo mensual programado │
└─────────────────────────────────────────────────────────┘
Información mostrada:
- Fecha y hora exacta del rebalanceo
- Número de posiciones modificadas
- Motivo/razón del rebalanceo
- Al hacer clic: detalles de cambios específicos
┌─────────────────────────────────────────────────────────┐
│ ⚠️ Alertas y Desviaciones │
├─────────────────────────────────────────────────────────┤
│ ⚠️ Drawdown significativo detectado: -12.5% │
│ │
│ ⚠️ Alta concentración en AAPL: 32.1% │
│ │
│ ℹ️ Underperformance vs benchmark: -2.3% │
└─────────────────────────────────────────────────────────┘
Tipos de alertas:
-
🔴 Warning (amarillo): Requieren atención
- Drawdown > 15%
- Concentración > 25% en un activo
- Deriva > 5% de pesos objetivo
-
🔵 Info (azul): Informativas
- Underperformance vs benchmark
- Cambios en volatilidad
- Oportunidades de rebalanceo
Flujo de trabajo:
graph LR
A[Escanear Mercado] --> B[Seleccionar Activos]
B --> C[Build Portfolio]
C --> D[Revisar Asignación]
D --> E[Click: Guardar Portfolio]
E --> F[Ingresar Nombre]
F --> G[Portfolio Guardado en IndexedDB]
G --> H[Aparece en Selector]
Código de uso:
// El usuario construye un portfolio en la UI
// Luego hace clic en "Guardar Portfolio"
// Internamente se ejecuta:
await portfolioManager.createPortfolio(
'Mi Portfolio Tech',
selectedAssets,
{
strategy: 'momentum_aggressive',
allocation_method: 'equal_weight',
benchmark: '^GSPC',
initial_capital: 10000
}
);Proceso:
- Usuario selecciona portfolio del dropdown
- Sistema carga precios actuales vía API
- Calcula P&L por posición:
P&L = (Precio Actual - Precio Entrada) × Cantidad P&L% = (Precio Actual - Precio Entrada) / Precio Entrada × 100 - Actualiza tabla y cards en tiempo real
Actualización automática:
- Al seleccionar portfolio
- Al hacer clic en "Actualizar"
- Al cambiar de tab de visualización
Algoritmo:
// 1. Obtener rango de fechas
const from = portfolio.created_at;
const to = today;
// 2. Para cada día laborable
for (const date of businessDays(from, to)) {
// 3. Obtener precios de todos los activos
const prices = await loadPricesForDate(date);
// 4. Calcular valor del portfolio
let totalValue = 0;
for (const position of portfolio.positions) {
totalValue += position.quantity * prices[position.ticker];
}
// 5. Agregar punto a la curva
equityCurve.push({
date,
value: totalValue,
return_pct: (totalValue - initialCapital) / initialCapital * 100
});
}Proceso:
- Cargar datos del benchmark (ej: S&P 500)
- Normalizar el benchmark al capital inicial
- Calcular métricas comparativas:
// Beta (sensibilidad al mercado)
beta = covariance(portfolioReturns, benchmarkReturns)
/ variance(benchmarkReturns)
// Alpha (exceso de retorno ajustado por riesgo)
alpha = portfolioReturn - (riskFreeRate + beta * benchmarkExcessReturn)
// Tracking Error (volatilidad de diferencia de retornos)
trackingError = std(portfolioReturns - benchmarkReturns)Funcionamiento:
Los snapshots capturan el estado del portfolio en un momento específico:
const snapshot = {
portfolio_id: 'abc123',
date: '2025-01-04',
positions: [
{ ticker: 'AAPL', price: 165.50, quantity: 50, value: 8275, weight: 0.32 },
// ... más posiciones
],
total_value: 25797,
daily_return: 0.85, // % de cambio desde ayer
cumulative_return: 5.42 // % de cambio desde inicio
};Ventajas:
- Histórico completo sin recalcular
- Análisis de tendencias a largo plazo
- Base para reportes y auditoría
Criterios de detección:
function needsRebalancing(portfolio, threshold = 0.05) {
for (const position of portfolio.positions) {
const drift = Math.abs(
position.current_weight - position.target_weight
);
if (drift > threshold) {
return true; // Necesita rebalanceo
}
}
return false;
}Ejecución:
- Detectar posiciones con deriva > 5%
- Calcular nuevas cantidades según pesos objetivo
- Registrar cambios en histórico:
{ ticker: 'AAPL', old_quantity: 50, new_quantity: 55, quantity_change: +5, reason: 'Drift del peso objetivo', price: 165.50 }
- Actualizar portfolio
- Crear nuevo snapshot
Retorno Total = (Valor Final - Valor Inicial) / Valor Inicial × 100
Retorno Anualizado = (1 + Retorno Total)^(252/Días) - 1
252 = número de días de trading al año
Sharpe = (Retorno Portfolio - Tasa Libre Riesgo) / Volatilidad Portfolio
Interpretación:
-
2.0: Excelente
- 1.0 - 2.0: Muy bueno
- 0.5 - 1.0: Bueno
- < 0.5: Pobre
Sortino = (Retorno Portfolio - Tasa Libre Riesgo) / Downside Deviation
Similar a Sharpe pero solo penaliza volatilidad negativa (más realista).
Calmar = Retorno Anualizado / |Max Drawdown|
Mide retorno vs peor caída histórica.
σ = √(Σ(retorno_i - retorno_promedio)² / n)
Anualizada: σ_anual = σ_diaria × √252
Max DD = (Trough Value - Peak Value) / Peak Value × 100
Identifica la peor caída desde un pico histórico.
VaR_95% = μ - 1.65 × σ
Pérdida máxima esperada en el peor 5% de casos (95% confianza).
CVaR = E[Pérdida | Pérdida > VaR]
Pérdida promedio cuando se supera el VaR (peor escenario).
β = Cov(r_portfolio, r_market) / Var(r_market)
Interpretación:
- β = 1: Se mueve igual que el mercado
- β > 1: Más volátil que el mercado
- β < 1: Menos volátil que el mercado
α = r_portfolio - [r_f + β × (r_market - r_f)]
Exceso de retorno no explicado por el mercado (skill del gestor).
{
id: string, // UUID único
name: string, // "Mi Portfolio Tech"
description: string, // Opcional
created_at: ISO8601, // "2025-01-04T10:30:00Z"
last_updated: ISO8601,
positions: [
{
ticker: string,
name: string,
sector: string,
entry_price: number,
entry_date: ISO8601,
quantity: number,
target_weight: number, // 0-1
current_weight: number, // 0-1
score: number, // Del análisis original
volatility: number
}
],
benchmark: string, // "^GSPC"
strategy: string, // "balanced"
allocation_method: string,// "equal_weight"
initial_capital: number, // 10000
current_value: number, // Actualizado con precios
total_return: number,
total_return_pct: number,
status: string // "active" | "closed" | "archived"
}Índices:
created_at: Para ordenar por fechalast_updated: Para encontrar portfolios recientesname: Para búsqueda por nombre
{
portfolio_id: string, // FK a portfolios
date: string, // "2025-01-04" (YYYY-MM-DD)
positions: [
{
ticker: string,
price: number,
quantity: number,
value: number,
weight: number,
unrealized_pnl: number,
unrealized_pnl_pct: number
}
],
total_value: number,
daily_return: number,
cumulative_return: number,
benchmark_value: number, // Opcional
benchmark_return: number // Opcional
}Clave primaria compuesta: [portfolio_id, date]
Índices:
portfolio_id: Para obtener snapshots de un portfoliodate: Para filtrar por rango de fechas
{
id: string,
portfolio_id: string,
timestamp: ISO8601,
reason: string, // "Drift > 5%", "Rebalanceo mensual"
before_positions: [...], // Estado antes del rebalanceo
after_positions: [...], // Estado después
changes: [
{
ticker: string,
old_quantity: number,
new_quantity: number,
quantity_change: number,
price: number,
old_weight: number,
new_weight: number
}
],
total_value: number
}Índices:
portfolio_id: Histórico por portfoliotimestamp: Ordenar cronológicamente
{
ticker: string,
date: string, // "2025-01-04"
price: number // Closing price
}Clave primaria compuesta: [ticker, date]
Propósito: Evitar llamadas repetidas a la API de Yahoo Finance.
1. Usuario → Escanea mercado US con estrategia "Momentum Aggressive"
2. Sistema → Analiza 500 acciones, devuelve top 50
3. Usuario → Selecciona 10 acciones con mejor score
4. Usuario → Click "Build Portfolio" con método "Equal Weight"
5. Sistema → Calcula pesos (10% cada acción)
6. Usuario → Click "Guardar Portfolio" en dashboard
7. Sistema → Prompt "Nombre del portfolio"
8. Usuario → Ingresa "Tech Leaders Q1 2025"
9. Sistema →
- Crea portfolio en IndexedDB
- Genera ID único
- Guarda posiciones con precios actuales
- Crea snapshot inicial
10. Sistema → Actualiza selector de portfolios
11. Sistema → Carga y muestra el dashboard
1. Usuario → Abre aplicación
2. Dashboard → Muestra selector con portfolios guardados
3. Usuario → Selecciona "Tech Leaders Q1 2025"
4. Sistema →
- Carga portfolio de IndexedDB
- Obtiene precios actuales (Yahoo API)
- Calcula P&L por posición
- Calcula métricas de rendimiento
- Genera equity curve desde snapshots
- Compara con benchmark
5. Dashboard → Renderiza:
- Cards con métricas actualizadas
- Tabla de posiciones con P&L
- Gráfico de equity curve
- Alertas si las hay
6. Usuario → Cambia a tab "vs Benchmark"
7. Sistema →
- Carga datos de ^GSPC
- Normaliza al capital inicial
- Calcula alpha y beta
- Renderiza gráfico comparativo
1. Sistema → Detecta drift en posiciones
- AAPL: peso actual 35% vs objetivo 10% → +25% drift
- TSLA: peso actual 8% vs objetivo 10% → -2% drift
2. Dashboard → Muestra alerta:
"⚠️ Alta concentración en AAPL: 35%"
3. Usuario → Revisa alertas y decide rebalancear
4. Sistema → Calcula nuevas cantidades:
- Valor total: $10,500
- AAPL: $3,675 → Debe ser $1,050 (10%)
- Vender: (3675-1050) / precio = X acciones
5. Usuario → Confirma (futura feature, por ahora manual)
6. Sistema →
- Registra rebalanceo en histórico
- Actualiza cantidades
- Crea nuevo snapshot
- Muestra cambios en histórico
import { portfolioManager } from '../portfolio/portfolio-manager.js';
// Crear portfolio
const portfolio = await portfolioManager.createPortfolio(
name, // string
assets, // array de assets del scanner
options // { strategy, allocation_method, benchmark, initial_capital }
);
// Cargar portfolio
const portfolio = await portfolioManager.loadPortfolio(id);
// Obtener todos los portfolios
const portfolios = await portfolioManager.getAllPortfolios();
// Actualizar portfolio
await portfolioManager.updatePortfolio(id, { current_value: 11000 });
// Eliminar portfolio
await portfolioManager.deletePortfolio(id);
// Agregar posición
await portfolioManager.addPosition(portfolioId, position);
// Remover posición
await portfolioManager.removePosition(portfolioId, ticker);
// Crear snapshot
await portfolioManager.createSnapshot(portfolio, priceData);
// Obtener equity curve
const curve = await portfolioManager.getEquityCurve(portfolioId);
// Ejecutar rebalanceo
const rebalance = await portfolioManager.executeRebalance(
portfolioId,
reason,
priceData
);
// Obtener histórico de rebalanceos
const history = await portfolioManager.getRebalanceHistory(portfolioId);import { performanceTracker } from '../portfolio/performance-tracker.js';
// Calcular P&L actual
const pnl = await performanceTracker.calculatePnL(portfolio);
// Retorna: { positions, total_value, total_pnl, total_pnl_pct }
// Calcular equity curve
const curve = await performanceTracker.calculateEquityCurve(
portfolio,
fromDate, // opcional
toDate // opcional
);
// Calcular drawdowns
const drawdowns = performanceTracker.calculateDrawdowns(equityCurve);
// Calcular max drawdown
const maxDD = performanceTracker.calculateMaxDrawdown(equityCurve);
// Calcular métricas de performance
const metrics = performanceTracker.calculatePerformanceMetrics(
equityCurve,
riskFreeRate // default: 0.02
);
// Retorna: { sharpe_ratio, sortino_ratio, calmar_ratio, etc. }
// Comparar con benchmark
const comparison = await performanceTracker.compareToBenchmark(
portfolio,
portfolioEquity
);
// Retorna: { alpha, beta, tracking_error, excess_return, etc. }
// Cargar precios actuales
const prices = await performanceTracker.loadCurrentPrices(tickers);import {
initDashboard,
loadPortfolio,
refreshDashboard,
switchChartTab
} from '../dashboard/portfolio-dashboard.js';
// Inicializar dashboard (automático en DOMContentLoaded)
await initDashboard();
// Cargar un portfolio específico
await loadPortfolio(portfolioId);
// Refrescar datos (actualizar precios y recalcular)
await refreshDashboard();
// Cambiar tab de visualización
switchChartTab('equity' | 'drawdown' | 'benchmark' | 'allocation');import { dbStore } from '../storage/indexed-db-store.js';
// Inicializar (automático en primera llamada)
await dbStore.init();
// Portfolios
await dbStore.savePortfolio(portfolio);
const portfolio = await dbStore.getPortfolio(id);
const all = await dbStore.getAllPortfolios();
await dbStore.deletePortfolio(id);
// Snapshots
await dbStore.saveSnapshot(snapshot);
const snapshots = await dbStore.getSnapshots(portfolioId, from, to);
// Rebalances
await dbStore.saveRebalance(rebalance);
const history = await dbStore.getRebalanceHistory(portfolioId);
// Price cache
await dbStore.savePriceCache(ticker, date, price);
const prices = await dbStore.getPriceCache(ticker, from, to);
// Utilidades
await dbStore.clearAll(); // ⚠️ Elimina todos los datos// En la UI, después de construir portfolio
document.getElementById('savePortfolioBtn').addEventListener('click', async () => {
const name = prompt('Ingresa un nombre para el portfolio:');
if (!name) return;
try {
// Obtener portfolio del estado global
const currentPortfolio = window.appState.portfolio;
if (!currentPortfolio) {
alert('Primero construye un portfolio');
return;
}
// Crear portfolio persistente
const portfolio = await portfolioManager.createPortfolio(
name,
currentPortfolio.assets,
{
strategy: window.appState.strategy || 'balanced',
allocation_method: currentPortfolio.allocation_method,
benchmark: window.appState.benchmark || '^GSPC',
initial_capital: 10000
}
);
alert('Portfolio guardado exitosamente!');
// Cargar en dashboard
await loadPortfolio(portfolio.id);
} catch (error) {
console.error('Error:', error);
alert('Error al guardar portfolio');
}
});async function displayPnL(portfolio) {
// Calcular P&L
const pnl = await performanceTracker.calculatePnL(portfolio);
// Actualizar card de resumen
document.getElementById('totalValueCard').textContent =
`$${pnl.total_value.toLocaleString('en-US', {
minimumFractionDigits: 2
})}`;
const returnCard = document.getElementById('totalReturnCard');
returnCard.textContent =
`${pnl.total_pnl_pct >= 0 ? '+' : ''}${pnl.total_pnl_pct.toFixed(2)}%`;
returnCard.style.color = pnl.total_pnl_pct >= 0 ? '#10b981' : '#ef4444';
// Actualizar tabla de posiciones
const tbody = document.getElementById('positionsTableBody');
tbody.innerHTML = pnl.positions.map(pos => {
const pnlColor = pos.unrealized_pnl >= 0 ? '#10b981' : '#ef4444';
return `
<tr>
<td>${pos.ticker}</td>
<td>${pos.name}</td>
<td>${pos.quantity}</td>
<td>$${pos.entry_price.toFixed(2)}</td>
<td>$${pos.current_price.toFixed(2)}</td>
<td>$${pos.current_value.toLocaleString()}</td>
<td>${pos.weight.toFixed(2)}%</td>
<td style="color: ${pnlColor}">
${pos.unrealized_pnl >= 0 ? '+' : ''}$${pos.unrealized_pnl.toFixed(2)}
</td>
<td style="color: ${pnlColor}">
${pos.unrealized_pnl_pct >= 0 ? '+' : ''}${pos.unrealized_pnl_pct.toFixed(2)}%
</td>
</tr>
`;
}).join('');
}function createEquityCurveChart(equityCurve) {
const canvas = document.getElementById('portfolioChart');
const ctx = canvas.getContext('2d');
return new Chart(ctx, {
type: 'line',
data: {
labels: equityCurve.map(p => p.date),
datasets: [{
label: 'Valor del Portfolio',
data: equityCurve.map(p => p.value),
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: { color: '#e2e8f0' }
},
tooltip: {
mode: 'index',
intersect: false,
backgroundColor: '#0f172a',
titleColor: '#e2e8f0',
bodyColor: '#94a3b8',
borderColor: '#334155',
borderWidth: 1,
callbacks: {
label: (context) => {
const value = context.parsed.y;
return `Valor: $${value.toLocaleString('en-US', {
minimumFractionDigits: 2
})}`;
}
}
}
},
scales: {
x: {
ticks: {
color: '#94a3b8',
maxTicksLimit: 10
},
grid: { color: '#334155' }
},
y: {
ticks: {
color: '#94a3b8',
callback: value => '$' + value.toLocaleString()
},
grid: { color: '#334155' }
}
}
}
});
}- ✅ Cachear precios: Usar
price_cachepara evitar llamadas repetidas a API - ✅ Lazy loading: Cargar gráficos solo cuando se cambia de tab
- ✅ Throttling: Limitar actualizaciones a máximo 1 por segundo
- ✅ Indexes: Usar índices de IndexedDB para queries rápidas
- ✅ Snapshots diarios: Guardar estado al final del día para histórico exacto
- ✅ Precios de cierre: Usar closing prices, no intraday
- ✅ Alineación de fechas: Solo business days en equity curve
- ✅ Manejo de splits/dividendos: Ajustar precios históricos (future feature)
- ✅ Feedback inmediato: Mostrar loading states durante cálculos
- ✅ Confirmaciones: Pedir confirmación antes de eliminar
- ✅ Tooltips: Explicar métricas complejas con hover text
- ✅ Responsive: Adaptar layout a mobile/tablet
- ✅ Client-side only: No exponer APIs sensibles
- ✅ Validación: Validar todos los inputs del usuario
- ✅ Sanitización: Escapar HTML en nombres de portfolios
- ✅ Rate limiting: Limitar llamadas a APIs externas
Solución:
- Verificar consola del navegador:
await dbStore.getAllPortfolios() - Revisar que IndexedDB está habilitado (no modo incógnito)
- Limpiar caché:
await dbStore.clearAll()y recrear
Solución:
- Verificar conectividad a Yahoo Finance API
- Revisar símbolos de tickers (deben ser válidos)
- Comprobar límite de rate limiting
- Ver logs de red en DevTools
Solución:
- Verificar que Chart.js se cargó correctamente
- Revisar consola de JavaScript por errores
- Asegurar que hay datos en
equityCurve - Verificar dimensiones del canvas (min-height: 400px)
Solución:
- Verificar que hay suficientes datos (mínimo 30 días)
- Revisar que los retornos están bien calculados
- Comprobar que fechas están alineadas
- Validar que no hay valores NaN o Infinity
-
Exportación de reportes
- PDF con resumen ejecutivo
- CSV de transacciones
- Excel con análisis detallado
-
Múltiples portfolios comparativos
- Vista side-by-side
- Performance relativa
- Consolidación de portfolios
-
Alertas configurables
- Threshold personalizado
- Notificaciones por email
- Webhooks a servicios externos
-
Análisis de atribución
- Retorno por sector
- Retorno por activo
- Retorno por factor (momentum, value, etc.)
-
Optimización de portfolios
- Markowitz mean-variance
- Black-Litterman
- Hierarchical Risk Parity
- Monte Carlo simulation
-
Integración con brokers
- Sincronización automática de posiciones
- Ejecución de órdenes
- Tracking de comisiones reales
El Dashboard de Portfolio Tracking es un sistema completo y profesional que permite a los usuarios gestionar sus inversiones con el mismo nivel de análisis que usan los profesionales institucionales.
Características clave:
- ✅ 100% local, sin necesidad de backend
- ✅ Métricas de nivel institucional
- ✅ Visualizaciones interactivas
- ✅ Multiidioma y responsive
- ✅ Extensible y modular
Para más información, consulta:
- Roadmap del proyecto
- Documentación de API
- Código fuente en
src/dashboard/,src/portfolio/,src/storage/
