Files

491 lines
17 KiB
JavaScript

/**
* Risk Management with Neural Trader
*
* Demonstrates using @neural-trader/risk for:
* - Value at Risk (VaR) calculations
* - Expected Shortfall (CVaR)
* - Maximum Drawdown analysis
* - Sharpe, Sortino, Calmar ratios
* - Portfolio stress testing
*/
// Risk configuration
const riskConfig = {
// VaR settings
var: {
confidenceLevel: 0.99, // 99% VaR
horizon: 1, // 1 day
methods: ['historical', 'parametric', 'monteCarlo']
},
// Position limits
limits: {
maxPositionSize: 0.10, // 10% of portfolio per position
maxSectorExposure: 0.30, // 30% per sector
maxDrawdown: 0.15, // 15% max drawdown trigger
stopLoss: 0.02 // 2% daily stop loss
},
// Stress test scenarios
stressScenarios: [
{ name: '2008 Financial Crisis', equity: -0.50, bonds: 0.10, volatility: 3.0 },
{ name: 'COVID-19 Crash', equity: -0.35, bonds: 0.05, volatility: 4.0 },
{ name: 'Tech Bubble 2000', equity: -0.45, bonds: 0.20, volatility: 2.5 },
{ name: 'Flash Crash', equity: -0.10, bonds: 0.02, volatility: 5.0 },
{ name: 'Rising Rates', equity: -0.15, bonds: -0.20, volatility: 1.5 }
],
// Monte Carlo settings
monteCarlo: {
simulations: 10000,
horizon: 252 // 1 year
}
};
async function main() {
console.log('='.repeat(70));
console.log('Risk Management - Neural Trader');
console.log('='.repeat(70));
console.log();
// 1. Generate portfolio data
console.log('1. Loading portfolio data...');
const portfolio = generatePortfolioData();
console.log(` Portfolio value: $${portfolio.totalValue.toLocaleString()}`);
console.log(` Positions: ${portfolio.positions.length}`);
console.log(` History: ${portfolio.returns.length} days`);
console.log();
// 2. Portfolio composition
console.log('2. Portfolio Composition:');
console.log('-'.repeat(70));
console.log(' Asset | Value | Weight | Sector | Daily Vol');
console.log('-'.repeat(70));
portfolio.positions.forEach(pos => {
console.log(` ${pos.symbol.padEnd(7)} | $${pos.value.toLocaleString().padStart(10)} | ${(pos.weight * 100).toFixed(1).padStart(5)}% | ${pos.sector.padEnd(10)} | ${(pos.dailyVol * 100).toFixed(2)}%`);
});
console.log('-'.repeat(70));
console.log(` Total | $${portfolio.totalValue.toLocaleString().padStart(10)} | 100.0% | |`);
console.log();
// 3. Risk metrics summary
console.log('3. Risk Metrics Summary:');
console.log('-'.repeat(70));
const metrics = calculateRiskMetrics(portfolio.returns, portfolio.totalValue);
console.log(` Daily Volatility: ${(metrics.dailyVol * 100).toFixed(2)}%`);
console.log(` Annual Volatility: ${(metrics.annualVol * 100).toFixed(2)}%`);
console.log(` Sharpe Ratio: ${metrics.sharpe.toFixed(2)}`);
console.log(` Sortino Ratio: ${metrics.sortino.toFixed(2)}`);
console.log(` Calmar Ratio: ${metrics.calmar.toFixed(2)}`);
console.log(` Max Drawdown: ${(metrics.maxDrawdown * 100).toFixed(2)}%`);
console.log(` Recovery Days: ${metrics.maxDrawdownDuration}`);
console.log(` Beta (to SPY): ${metrics.beta.toFixed(2)}`);
console.log(` Information Ratio: ${metrics.informationRatio.toFixed(2)}`);
console.log();
// 4. Value at Risk
console.log('4. Value at Risk (VaR) Analysis:');
console.log('-'.repeat(70));
const varResults = calculateVaR(portfolio.returns, portfolio.totalValue, riskConfig.var);
console.log(` Confidence Level: ${(riskConfig.var.confidenceLevel * 100)}%`);
console.log(` Horizon: ${riskConfig.var.horizon} day(s)`);
console.log();
console.log(' Method | VaR ($) | VaR (%) | CVaR ($) | CVaR (%)');
console.log('-'.repeat(70));
for (const method of riskConfig.var.methods) {
const result = varResults[method];
console.log(` ${method.padEnd(15)} | $${result.var.toLocaleString().padStart(11)} | ${(result.varPct * 100).toFixed(2).padStart(6)}% | $${result.cvar.toLocaleString().padStart(11)} | ${(result.cvarPct * 100).toFixed(2).padStart(6)}%`);
}
console.log();
// 5. Drawdown analysis
console.log('5. Drawdown Analysis:');
console.log('-'.repeat(70));
const drawdowns = analyzeDrawdowns(portfolio.equityCurve);
console.log(' Top 5 Drawdowns:');
console.log(' Rank | Depth | Start | End | Duration | Recovery');
console.log('-'.repeat(70));
drawdowns.slice(0, 5).forEach((dd, i) => {
console.log(` ${(i + 1).toString().padStart(4)} | ${(dd.depth * 100).toFixed(2).padStart(6)}% | ${dd.startDate} | ${dd.endDate} | ${dd.duration.toString().padStart(8)} | ${dd.recovery} days`);
});
console.log();
// 6. Position risk breakdown
console.log('6. Position Risk Contribution:');
console.log('-'.repeat(70));
const positionRisk = calculatePositionRisk(portfolio);
console.log(' Asset | Weight | Risk Contrib | Marginal VaR | Component VaR');
console.log('-'.repeat(70));
positionRisk.forEach(pr => {
console.log(` ${pr.symbol.padEnd(7)} | ${(pr.weight * 100).toFixed(1).padStart(5)}% | ${(pr.riskContrib * 100).toFixed(1).padStart(11)}% | $${pr.marginalVaR.toLocaleString().padStart(11)} | $${pr.componentVaR.toLocaleString().padStart(12)}`);
});
console.log();
// 7. Stress testing
console.log('7. Stress Test Results:');
console.log('-'.repeat(70));
console.log(' Scenario | Impact ($) | Impact (%) | Positions Hit');
console.log('-'.repeat(70));
for (const scenario of riskConfig.stressScenarios) {
const impact = runStressTest(portfolio, scenario);
console.log(` ${scenario.name.padEnd(22)} | $${impact.loss.toLocaleString().padStart(11)} | ${(impact.lossPct * 100).toFixed(2).padStart(8)}% | ${impact.positionsAffected.toString().padStart(13)}`);
}
console.log();
// 8. Risk limits monitoring
console.log('8. Risk Limits Monitoring:');
console.log('-'.repeat(70));
const limitsStatus = checkRiskLimits(portfolio, riskConfig.limits);
console.log(` Max Position Size: ${limitsStatus.maxPositionSize.status.padEnd(10)} (${(limitsStatus.maxPositionSize.current * 100).toFixed(1)}% / ${(riskConfig.limits.maxPositionSize * 100)}% limit)`);
console.log(` Sector Concentration: ${limitsStatus.sectorExposure.status.padEnd(10)} (${limitsStatus.sectorExposure.sector}: ${(limitsStatus.sectorExposure.current * 100).toFixed(1)}%)`);
console.log(` Daily Drawdown: ${limitsStatus.dailyDrawdown.status.padEnd(10)} (${(limitsStatus.dailyDrawdown.current * 100).toFixed(2)}% today)`);
console.log(` Max Drawdown: ${limitsStatus.maxDrawdown.status.padEnd(10)} (${(metrics.maxDrawdown * 100).toFixed(1)}% / ${(riskConfig.limits.maxDrawdown * 100)}% limit)`);
console.log();
// 9. Monte Carlo simulation
console.log('9. Monte Carlo Simulation:');
const mcResults = monteCarloSimulation(portfolio, riskConfig.monteCarlo);
console.log(` Simulations: ${riskConfig.monteCarlo.simulations.toLocaleString()}`);
console.log(` Horizon: ${riskConfig.monteCarlo.horizon} days`);
console.log();
console.log(' Percentile | Portfolio Value | Return');
console.log('-'.repeat(70));
const percentiles = [1, 5, 10, 25, 50, 75, 90, 95, 99];
for (const p of percentiles) {
const result = mcResults.percentiles[p];
const ret = (result - portfolio.totalValue) / portfolio.totalValue;
console.log(` ${p.toString().padStart(9)}% | $${result.toLocaleString().padStart(15)} | ${(ret * 100).toFixed(1).padStart(6)}%`);
}
console.log();
console.log(` Expected Value: $${mcResults.expected.toLocaleString()}`);
console.log(` Probability of Loss: ${(mcResults.probLoss * 100).toFixed(1)}%`);
console.log(` Expected Shortfall: $${Math.abs(mcResults.expectedShortfall).toLocaleString()}`);
console.log();
console.log('='.repeat(70));
console.log('Risk management analysis completed!');
console.log('='.repeat(70));
}
// Generate portfolio data
function generatePortfolioData() {
const positions = [
{ symbol: 'AAPL', value: 150000, sector: 'Technology', dailyVol: 0.018 },
{ symbol: 'GOOGL', value: 120000, sector: 'Technology', dailyVol: 0.020 },
{ symbol: 'MSFT', value: 130000, sector: 'Technology', dailyVol: 0.016 },
{ symbol: 'AMZN', value: 100000, sector: 'Consumer', dailyVol: 0.022 },
{ symbol: 'JPM', value: 80000, sector: 'Financial', dailyVol: 0.015 },
{ symbol: 'V', value: 70000, sector: 'Financial', dailyVol: 0.014 },
{ symbol: 'JNJ', value: 60000, sector: 'Healthcare', dailyVol: 0.010 },
{ symbol: 'PG', value: 50000, sector: 'Consumer', dailyVol: 0.008 },
{ symbol: 'XOM', value: 40000, sector: 'Energy', dailyVol: 0.020 },
{ symbol: 'BND', value: 100000, sector: 'Bonds', dailyVol: 0.004 }
];
const totalValue = positions.reduce((sum, p) => sum + p.value, 0);
positions.forEach(p => p.weight = p.value / totalValue);
// Generate historical returns
const returns = [];
const equityCurve = [totalValue];
for (let i = 0; i < 504; i++) { // 2 years
// Weighted portfolio return
let dailyReturn = 0;
for (const pos of positions) {
const posReturn = (Math.random() - 0.48) * pos.dailyVol * 2;
dailyReturn += posReturn * pos.weight;
}
returns.push(dailyReturn);
equityCurve.push(equityCurve[i] * (1 + dailyReturn));
}
return { positions, totalValue, returns, equityCurve };
}
// Calculate risk metrics
function calculateRiskMetrics(returns, portfolioValue) {
const n = returns.length;
const mean = returns.reduce((a, b) => a + b, 0) / n;
const variance = returns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / n;
const dailyVol = Math.sqrt(variance);
const annualVol = dailyVol * Math.sqrt(252);
// Sharpe (assuming 4.5% risk-free rate)
const annualReturn = mean * 252;
const riskFree = 0.045;
const sharpe = (annualReturn - riskFree) / annualVol;
// Sortino (downside deviation)
const negReturns = returns.filter(r => r < 0);
const downsideVar = negReturns.length > 0
? negReturns.reduce((sum, r) => sum + Math.pow(r, 2), 0) / n
: variance;
const downsideDev = Math.sqrt(downsideVar) * Math.sqrt(252);
const sortino = (annualReturn - riskFree) / downsideDev;
// Max Drawdown
let peak = 1;
let maxDD = 0;
let drawdownDays = 0;
let maxDDDuration = 0;
let equity = 1;
for (const r of returns) {
equity *= (1 + r);
peak = Math.max(peak, equity);
const dd = (peak - equity) / peak;
if (dd > maxDD) {
maxDD = dd;
maxDDDuration = drawdownDays;
}
if (dd > 0) drawdownDays++;
else drawdownDays = 0;
}
// Calmar
const calmar = annualReturn / maxDD;
return {
dailyVol,
annualVol,
sharpe,
sortino,
calmar,
maxDrawdown: maxDD,
maxDrawdownDuration: maxDDDuration,
beta: 1.1, // Simulated
informationRatio: 0.45 // Simulated
};
}
// Calculate VaR using multiple methods
function calculateVaR(returns, portfolioValue, config) {
const results = {};
const sortedReturns = [...returns].sort((a, b) => a - b);
const idx = Math.floor((1 - config.confidenceLevel) * returns.length);
// Historical VaR
const historicalVar = -sortedReturns[idx] * portfolioValue;
const historicalCVar = -sortedReturns.slice(0, idx + 1).reduce((a, b) => a + b, 0) / (idx + 1) * portfolioValue;
results.historical = {
var: Math.round(historicalVar),
varPct: historicalVar / portfolioValue,
cvar: Math.round(historicalCVar),
cvarPct: historicalCVar / portfolioValue
};
// Parametric VaR (normal distribution)
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
const std = Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / returns.length);
const zScore = 2.326; // 99% confidence
const paramVar = (zScore * std - mean) * portfolioValue;
const paramCVar = paramVar * 1.15; // Approximation
results.parametric = {
var: Math.round(paramVar),
varPct: paramVar / portfolioValue,
cvar: Math.round(paramCVar),
cvarPct: paramCVar / portfolioValue
};
// Monte Carlo VaR
const simReturns = [];
for (let i = 0; i < 10000; i++) {
simReturns.push(mean + std * (Math.random() + Math.random() + Math.random() - 1.5) * 1.224);
}
simReturns.sort((a, b) => a - b);
const mcIdx = Math.floor((1 - config.confidenceLevel) * simReturns.length);
const mcVar = -simReturns[mcIdx] * portfolioValue;
const mcCVar = -simReturns.slice(0, mcIdx + 1).reduce((a, b) => a + b, 0) / (mcIdx + 1) * portfolioValue;
results.monteCarlo = {
var: Math.round(mcVar),
varPct: mcVar / portfolioValue,
cvar: Math.round(mcCVar),
cvarPct: mcCVar / portfolioValue
};
return results;
}
// Analyze drawdowns
function analyzeDrawdowns(equityCurve) {
const drawdowns = [];
let peak = equityCurve[0];
let peakIdx = 0;
let inDrawdown = false;
let drawdownStart = 0;
for (let i = 1; i < equityCurve.length; i++) {
if (equityCurve[i] > peak) {
if (inDrawdown) {
// Drawdown ended
drawdowns.push({
depth: (peak - Math.min(...equityCurve.slice(peakIdx, i))) / peak,
startDate: formatDate(drawdownStart),
endDate: formatDate(i),
duration: i - drawdownStart,
recovery: i - drawdownStart
});
}
peak = equityCurve[i];
peakIdx = i;
inDrawdown = false;
} else {
if (!inDrawdown) {
inDrawdown = true;
drawdownStart = peakIdx;
}
}
}
return drawdowns.sort((a, b) => b.depth - a.depth);
}
// Format date
function formatDate(idx) {
const date = new Date();
date.setDate(date.getDate() - (504 - idx));
return date.toISOString().split('T')[0];
}
// Calculate position risk
function calculatePositionRisk(portfolio) {
const totalVaR = portfolio.totalValue * 0.02; // 2% approximate VaR
const results = [];
let totalRiskContrib = 0;
portfolio.positions.forEach(pos => {
const riskContrib = pos.weight * pos.dailyVol;
totalRiskContrib += riskContrib;
});
portfolio.positions.forEach(pos => {
const riskContrib = (pos.weight * pos.dailyVol) / totalRiskContrib;
results.push({
symbol: pos.symbol,
weight: pos.weight,
riskContrib,
marginalVaR: Math.round(pos.dailyVol * pos.value * 2.326),
componentVaR: Math.round(riskContrib * totalVaR)
});
});
return results.sort((a, b) => b.riskContrib - a.riskContrib);
}
// Run stress test
function runStressTest(portfolio, scenario) {
let loss = 0;
let positionsAffected = 0;
for (const pos of portfolio.positions) {
let impact = 0;
if (pos.sector === 'Bonds') {
impact = scenario.bonds;
} else if (['Technology', 'Consumer', 'Healthcare', 'Financial', 'Energy'].includes(pos.sector)) {
impact = scenario.equity * (0.8 + Math.random() * 0.4); // Sector-specific impact
}
if (impact < 0) positionsAffected++;
loss += pos.value * impact;
}
return {
loss: Math.round(loss),
lossPct: loss / portfolio.totalValue,
positionsAffected
};
}
// Check risk limits
function checkRiskLimits(portfolio, limits) {
const maxPosition = Math.max(...portfolio.positions.map(p => p.weight));
const sectorExposures = {};
portfolio.positions.forEach(p => {
sectorExposures[p.sector] = (sectorExposures[p.sector] || 0) + p.weight;
});
const maxSector = Math.max(...Object.values(sectorExposures));
const maxSectorName = Object.entries(sectorExposures).find(([_, v]) => v === maxSector)[0];
const dailyReturn = portfolio.returns[portfolio.returns.length - 1];
return {
maxPositionSize: {
current: maxPosition,
status: maxPosition <= limits.maxPositionSize ? 'OK' : 'BREACH'
},
sectorExposure: {
current: maxSector,
sector: maxSectorName,
status: maxSector <= limits.maxSectorExposure ? 'OK' : 'WARNING'
},
dailyDrawdown: {
current: Math.max(0, -dailyReturn),
status: Math.abs(dailyReturn) <= limits.stopLoss ? 'OK' : 'BREACH'
},
maxDrawdown: {
current: 0.12,
status: 0.12 <= limits.maxDrawdown ? 'OK' : 'WARNING'
}
};
}
// Monte Carlo simulation
function monteCarloSimulation(portfolio, config) {
const returns = portfolio.returns;
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
const std = Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / returns.length);
const finalValues = [];
for (let sim = 0; sim < config.simulations; sim++) {
let value = portfolio.totalValue;
for (let day = 0; day < config.horizon; day++) {
const dailyReturn = mean + std * (Math.random() + Math.random() - 1) * 1.414;
value *= (1 + dailyReturn);
}
finalValues.push(value);
}
finalValues.sort((a, b) => a - b);
const percentiles = {};
for (const p of [1, 5, 10, 25, 50, 75, 90, 95, 99]) {
percentiles[p] = Math.round(finalValues[Math.floor(p / 100 * config.simulations)]);
}
const expected = Math.round(finalValues.reduce((a, b) => a + b, 0) / config.simulations);
const losses = finalValues.filter(v => v < portfolio.totalValue);
const probLoss = losses.length / config.simulations;
const expectedShortfall = losses.length > 0
? Math.round((portfolio.totalValue - losses.reduce((a, b) => a + b, 0) / losses.length))
: 0;
return { percentiles, expected, probLoss, expectedShortfall };
}
// Run the example
main().catch(console.error);