Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
428
vendor/ruvector/examples/neural-trader/portfolio/optimization.js
vendored
Normal file
428
vendor/ruvector/examples/neural-trader/portfolio/optimization.js
vendored
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* Portfolio Optimization with Neural Trader
|
||||
*
|
||||
* Demonstrates using @neural-trader/portfolio for:
|
||||
* - Mean-Variance Optimization (Markowitz)
|
||||
* - Risk Parity Portfolio
|
||||
* - Maximum Sharpe Ratio
|
||||
* - Minimum Volatility
|
||||
* - Black-Litterman Model
|
||||
*/
|
||||
|
||||
// Portfolio configuration
|
||||
const portfolioConfig = {
|
||||
// Assets to optimize
|
||||
assets: ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'NVDA', 'META', 'TSLA', 'BRK.B', 'JPM', 'V'],
|
||||
|
||||
// Risk-free rate (annual)
|
||||
riskFreeRate: 0.045,
|
||||
|
||||
// Optimization constraints
|
||||
constraints: {
|
||||
minWeight: 0.02, // Minimum 2% per asset
|
||||
maxWeight: 0.25, // Maximum 25% per asset
|
||||
maxSectorWeight: 0.40, // Maximum 40% per sector
|
||||
turnoverLimit: 0.20 // Maximum 20% turnover per rebalance
|
||||
},
|
||||
|
||||
// Lookback period for historical data
|
||||
lookbackDays: 252 * 3 // 3 years
|
||||
};
|
||||
|
||||
// Sector mappings
|
||||
const sectorMap = {
|
||||
'AAPL': 'Technology', 'GOOGL': 'Technology', 'MSFT': 'Technology',
|
||||
'AMZN': 'Consumer', 'NVDA': 'Technology', 'META': 'Technology',
|
||||
'TSLA': 'Consumer', 'BRK.B': 'Financial', 'JPM': 'Financial', 'V': 'Financial'
|
||||
};
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(70));
|
||||
console.log('Portfolio Optimization - Neural Trader');
|
||||
console.log('='.repeat(70));
|
||||
console.log();
|
||||
|
||||
// 1. Load historical returns
|
||||
console.log('1. Loading historical data...');
|
||||
const { returns, prices, covariance, expectedReturns } = generateHistoricalData(
|
||||
portfolioConfig.assets,
|
||||
portfolioConfig.lookbackDays
|
||||
);
|
||||
console.log(` Assets: ${portfolioConfig.assets.length}`);
|
||||
console.log(` Data points: ${portfolioConfig.lookbackDays} days`);
|
||||
console.log();
|
||||
|
||||
// 2. Display asset statistics
|
||||
console.log('2. Asset Statistics:');
|
||||
console.log('-'.repeat(70));
|
||||
console.log(' Asset | Ann. Return | Volatility | Sharpe | Sector');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
portfolioConfig.assets.forEach(asset => {
|
||||
const annReturn = expectedReturns[asset];
|
||||
const vol = Math.sqrt(covariance[asset][asset]) * Math.sqrt(252);
|
||||
const sharpe = (annReturn - portfolioConfig.riskFreeRate) / vol;
|
||||
|
||||
console.log(` ${asset.padEnd(7)} | ${(annReturn * 100).toFixed(1).padStart(10)}% | ${(vol * 100).toFixed(1).padStart(9)}% | ${sharpe.toFixed(2).padStart(6)} | ${sectorMap[asset]}`);
|
||||
});
|
||||
console.log();
|
||||
|
||||
// 3. Calculate different portfolio optimizations
|
||||
console.log('3. Portfolio Optimization Results:');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
// Equal Weight (benchmark)
|
||||
const equalWeight = equalWeightPortfolio(portfolioConfig.assets);
|
||||
displayPortfolio('Equal Weight (Benchmark)', equalWeight, expectedReturns, covariance);
|
||||
|
||||
// Minimum Variance
|
||||
const minVar = minimumVariancePortfolio(expectedReturns, covariance, portfolioConfig.constraints);
|
||||
displayPortfolio('Minimum Variance', minVar, expectedReturns, covariance);
|
||||
|
||||
// Maximum Sharpe Ratio
|
||||
const maxSharpe = maximumSharpePortfolio(expectedReturns, covariance, portfolioConfig.riskFreeRate, portfolioConfig.constraints);
|
||||
displayPortfolio('Maximum Sharpe Ratio', maxSharpe, expectedReturns, covariance);
|
||||
|
||||
// Risk Parity
|
||||
const riskParity = riskParityPortfolio(covariance);
|
||||
displayPortfolio('Risk Parity', riskParity, expectedReturns, covariance);
|
||||
|
||||
// Black-Litterman
|
||||
const bl = blackLittermanPortfolio(expectedReturns, covariance, portfolioConfig.constraints);
|
||||
displayPortfolio('Black-Litterman', bl, expectedReturns, covariance);
|
||||
|
||||
// 4. Efficient Frontier
|
||||
console.log('4. Efficient Frontier:');
|
||||
console.log('-'.repeat(70));
|
||||
console.log(' Target Vol | Exp. Return | Sharpe | Weights Summary');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
const targetVols = [0.10, 0.12, 0.15, 0.18, 0.20, 0.25];
|
||||
for (const targetVol of targetVols) {
|
||||
const portfolio = efficientFrontierPoint(expectedReturns, covariance, targetVol, portfolioConfig.constraints);
|
||||
const ret = calculatePortfolioReturn(portfolio, expectedReturns);
|
||||
const vol = calculatePortfolioVolatility(portfolio, covariance);
|
||||
const sharpe = (ret - portfolioConfig.riskFreeRate) / vol;
|
||||
|
||||
// Summarize weights
|
||||
const topWeights = Object.entries(portfolio)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 3)
|
||||
.map(([asset, weight]) => `${asset}:${(weight * 100).toFixed(0)}%`)
|
||||
.join(', ');
|
||||
|
||||
console.log(` ${(targetVol * 100).toFixed(0).padStart(9)}% | ${(ret * 100).toFixed(1).padStart(10)}% | ${sharpe.toFixed(2).padStart(6)} | ${topWeights}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// 5. Sector allocation analysis
|
||||
console.log('5. Sector Allocation Analysis:');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
const portfolios = {
|
||||
'Equal Weight': equalWeight,
|
||||
'Min Variance': minVar,
|
||||
'Max Sharpe': maxSharpe,
|
||||
'Risk Parity': riskParity
|
||||
};
|
||||
|
||||
const sectors = [...new Set(Object.values(sectorMap))];
|
||||
console.log(` Portfolio | ${sectors.map(s => s.padEnd(10)).join(' | ')}`);
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
for (const [name, portfolio] of Object.entries(portfolios)) {
|
||||
const sectorWeights = {};
|
||||
sectors.forEach(s => sectorWeights[s] = 0);
|
||||
|
||||
for (const [asset, weight] of Object.entries(portfolio)) {
|
||||
sectorWeights[sectorMap[asset]] += weight;
|
||||
}
|
||||
|
||||
const row = sectors.map(s => (sectorWeights[s] * 100).toFixed(1).padStart(8) + '%').join(' | ');
|
||||
console.log(` ${name.padEnd(14)} | ${row}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// 6. Rebalancing analysis
|
||||
console.log('6. Rebalancing Analysis (from Equal Weight):');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
for (const [name, portfolio] of Object.entries(portfolios)) {
|
||||
if (name === 'Equal Weight') continue;
|
||||
|
||||
let turnover = 0;
|
||||
for (const asset of portfolioConfig.assets) {
|
||||
turnover += Math.abs((portfolio[asset] || 0) - equalWeight[asset]);
|
||||
}
|
||||
turnover /= 2; // One-way turnover
|
||||
|
||||
const numTrades = Object.keys(portfolio).filter(a =>
|
||||
Math.abs((portfolio[a] || 0) - equalWeight[a]) > 0.01
|
||||
).length;
|
||||
|
||||
console.log(` ${name.padEnd(15)}: ${(turnover * 100).toFixed(1)}% turnover, ${numTrades} trades required`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// 7. Risk decomposition
|
||||
console.log('7. Risk Decomposition (Max Sharpe Portfolio):');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
const riskContrib = calculateRiskContribution(maxSharpe, covariance);
|
||||
console.log(' Asset | Weight | Risk Contrib | Marginal Risk');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
Object.entries(riskContrib)
|
||||
.sort((a, b) => b[1].contribution - a[1].contribution)
|
||||
.forEach(([asset, { weight, contribution, marginal }]) => {
|
||||
console.log(` ${asset.padEnd(7)} | ${(weight * 100).toFixed(1).padStart(5)}% | ${(contribution * 100).toFixed(1).padStart(11)}% | ${(marginal * 100).toFixed(2).padStart(12)}%`);
|
||||
});
|
||||
console.log();
|
||||
|
||||
console.log('='.repeat(70));
|
||||
console.log('Portfolio optimization completed!');
|
||||
console.log('='.repeat(70));
|
||||
}
|
||||
|
||||
// Generate historical data
|
||||
function generateHistoricalData(assets, days) {
|
||||
const prices = {};
|
||||
const returns = {};
|
||||
const expectedReturns = {};
|
||||
const covariance = {};
|
||||
|
||||
// Initialize covariance matrix
|
||||
assets.forEach(a => {
|
||||
covariance[a] = {};
|
||||
assets.forEach(b => covariance[a][b] = 0);
|
||||
});
|
||||
|
||||
// Generate correlated returns
|
||||
for (const asset of assets) {
|
||||
prices[asset] = [100 + Math.random() * 200];
|
||||
returns[asset] = [];
|
||||
|
||||
// Generate random returns with realistic characteristics
|
||||
const annualReturn = 0.08 + Math.random() * 0.15; // 8-23% annual return
|
||||
const dailyReturn = annualReturn / 252;
|
||||
const dailyVol = (0.15 + Math.random() * 0.25) / Math.sqrt(252);
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
const r = dailyReturn + dailyVol * (Math.random() - 0.5) * 2;
|
||||
returns[asset].push(r);
|
||||
prices[asset].push(prices[asset][i] * (1 + r));
|
||||
}
|
||||
|
||||
// Calculate expected return (annualized)
|
||||
const avgReturn = returns[asset].reduce((a, b) => a + b, 0) / returns[asset].length;
|
||||
expectedReturns[asset] = avgReturn * 252;
|
||||
}
|
||||
|
||||
// Calculate covariance matrix
|
||||
for (const a of assets) {
|
||||
for (const b of assets) {
|
||||
if (a === b) {
|
||||
// Variance
|
||||
const mean = returns[a].reduce((s, r) => s + r, 0) / returns[a].length;
|
||||
covariance[a][b] = returns[a].reduce((s, r) => s + Math.pow(r - mean, 2), 0) / returns[a].length;
|
||||
} else {
|
||||
// Covariance with correlation factor
|
||||
const meanA = returns[a].reduce((s, r) => s + r, 0) / returns[a].length;
|
||||
const meanB = returns[b].reduce((s, r) => s + r, 0) / returns[b].length;
|
||||
|
||||
let cov = 0;
|
||||
for (let i = 0; i < days; i++) {
|
||||
cov += (returns[a][i] - meanA) * (returns[b][i] - meanB);
|
||||
}
|
||||
cov /= days;
|
||||
|
||||
// Add sector correlation
|
||||
const sameSecter = sectorMap[a] === sectorMap[b];
|
||||
const corrFactor = sameSecter ? 1.5 : 0.8;
|
||||
covariance[a][b] = cov * corrFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { returns, prices, covariance, expectedReturns };
|
||||
}
|
||||
|
||||
// Equal weight portfolio
|
||||
function equalWeightPortfolio(assets) {
|
||||
const weight = 1 / assets.length;
|
||||
const portfolio = {};
|
||||
assets.forEach(a => portfolio[a] = weight);
|
||||
return portfolio;
|
||||
}
|
||||
|
||||
// Minimum variance portfolio (simplified)
|
||||
function minimumVariancePortfolio(expectedReturns, covariance, constraints) {
|
||||
const assets = Object.keys(expectedReturns);
|
||||
const n = assets.length;
|
||||
|
||||
// Simple optimization: inversely proportional to variance
|
||||
const invVariances = assets.map(a => 1 / covariance[a][a]);
|
||||
const sum = invVariances.reduce((a, b) => a + b, 0);
|
||||
|
||||
const portfolio = {};
|
||||
assets.forEach((a, i) => {
|
||||
let weight = invVariances[i] / sum;
|
||||
weight = Math.max(constraints.minWeight, Math.min(constraints.maxWeight, weight));
|
||||
portfolio[a] = weight;
|
||||
});
|
||||
|
||||
// Normalize to sum to 1
|
||||
const totalWeight = Object.values(portfolio).reduce((a, b) => a + b, 0);
|
||||
Object.keys(portfolio).forEach(a => portfolio[a] /= totalWeight);
|
||||
|
||||
return portfolio;
|
||||
}
|
||||
|
||||
// Maximum Sharpe ratio portfolio (simplified)
|
||||
function maximumSharpePortfolio(expectedReturns, covariance, riskFreeRate, constraints) {
|
||||
const assets = Object.keys(expectedReturns);
|
||||
|
||||
// Simple optimization: proportional to excess return / variance
|
||||
const scores = assets.map(a => {
|
||||
const excessReturn = expectedReturns[a] - riskFreeRate;
|
||||
const vol = Math.sqrt(covariance[a][a]) * Math.sqrt(252);
|
||||
return Math.max(0, excessReturn / vol);
|
||||
});
|
||||
|
||||
const sum = scores.reduce((a, b) => a + b, 0);
|
||||
|
||||
const portfolio = {};
|
||||
assets.forEach((a, i) => {
|
||||
let weight = sum > 0 ? scores[i] / sum : 1 / assets.length;
|
||||
weight = Math.max(constraints.minWeight, Math.min(constraints.maxWeight, weight));
|
||||
portfolio[a] = weight;
|
||||
});
|
||||
|
||||
// Normalize
|
||||
const totalWeight = Object.values(portfolio).reduce((a, b) => a + b, 0);
|
||||
Object.keys(portfolio).forEach(a => portfolio[a] /= totalWeight);
|
||||
|
||||
return portfolio;
|
||||
}
|
||||
|
||||
// Risk parity portfolio
|
||||
function riskParityPortfolio(covariance) {
|
||||
const assets = Object.keys(covariance);
|
||||
|
||||
// Target: equal risk contribution
|
||||
// Simplified: inversely proportional to volatility
|
||||
const invVols = assets.map(a => 1 / Math.sqrt(covariance[a][a]));
|
||||
const sum = invVols.reduce((a, b) => a + b, 0);
|
||||
|
||||
const portfolio = {};
|
||||
assets.forEach((a, i) => portfolio[a] = invVols[i] / sum);
|
||||
|
||||
return portfolio;
|
||||
}
|
||||
|
||||
// Black-Litterman portfolio (simplified)
|
||||
function blackLittermanPortfolio(expectedReturns, covariance, constraints) {
|
||||
const assets = Object.keys(expectedReturns);
|
||||
|
||||
// Views: slight adjustment to expected returns based on "views"
|
||||
const adjustedReturns = {};
|
||||
assets.forEach(a => {
|
||||
// Simulate analyst view adjustment
|
||||
const viewAdjustment = (Math.random() - 0.5) * 0.02;
|
||||
adjustedReturns[a] = expectedReturns[a] + viewAdjustment;
|
||||
});
|
||||
|
||||
return maximumSharpePortfolio(adjustedReturns, covariance, portfolioConfig.riskFreeRate, constraints);
|
||||
}
|
||||
|
||||
// Efficient frontier point
|
||||
function efficientFrontierPoint(expectedReturns, covariance, targetVol, constraints) {
|
||||
// Simplified: interpolate between min variance and max return
|
||||
const minVar = minimumVariancePortfolio(expectedReturns, covariance, constraints);
|
||||
const maxSharpe = maximumSharpePortfolio(expectedReturns, covariance, portfolioConfig.riskFreeRate, constraints);
|
||||
|
||||
const minVol = calculatePortfolioVolatility(minVar, covariance);
|
||||
const maxVol = calculatePortfolioVolatility(maxSharpe, covariance);
|
||||
|
||||
const alpha = Math.min(1, Math.max(0, (targetVol - minVol) / (maxVol - minVol)));
|
||||
|
||||
const portfolio = {};
|
||||
Object.keys(minVar).forEach(a => {
|
||||
portfolio[a] = minVar[a] * (1 - alpha) + maxSharpe[a] * alpha;
|
||||
});
|
||||
|
||||
return portfolio;
|
||||
}
|
||||
|
||||
// Calculate portfolio return
|
||||
function calculatePortfolioReturn(portfolio, expectedReturns) {
|
||||
let ret = 0;
|
||||
for (const [asset, weight] of Object.entries(portfolio)) {
|
||||
ret += weight * expectedReturns[asset];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Calculate portfolio volatility
|
||||
function calculatePortfolioVolatility(portfolio, covariance) {
|
||||
const assets = Object.keys(portfolio);
|
||||
let variance = 0;
|
||||
|
||||
for (const a of assets) {
|
||||
for (const b of assets) {
|
||||
variance += portfolio[a] * portfolio[b] * covariance[a][b] * 252;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
// Calculate risk contribution
|
||||
function calculateRiskContribution(portfolio, covariance) {
|
||||
const assets = Object.keys(portfolio);
|
||||
const totalVol = calculatePortfolioVolatility(portfolio, covariance);
|
||||
|
||||
const result = {};
|
||||
|
||||
for (const asset of assets) {
|
||||
// Marginal contribution to risk
|
||||
let marginal = 0;
|
||||
for (const b of assets) {
|
||||
marginal += portfolio[b] * covariance[asset][b] * 252;
|
||||
}
|
||||
marginal /= totalVol;
|
||||
|
||||
// Total contribution
|
||||
const contribution = portfolio[asset] * marginal / totalVol;
|
||||
|
||||
result[asset] = {
|
||||
weight: portfolio[asset],
|
||||
contribution,
|
||||
marginal
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Display portfolio summary
|
||||
function displayPortfolio(name, portfolio, expectedReturns, covariance) {
|
||||
console.log(`\n ${name}:`);
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
// Sort by weight
|
||||
const sorted = Object.entries(portfolio).sort((a, b) => b[1] - a[1]);
|
||||
|
||||
console.log(' Weights: ' + sorted.slice(0, 5).map(([a, w]) => `${a}:${(w * 100).toFixed(1)}%`).join(', ') + (sorted.length > 5 ? '...' : ''));
|
||||
|
||||
const ret = calculatePortfolioReturn(portfolio, expectedReturns);
|
||||
const vol = calculatePortfolioVolatility(portfolio, covariance);
|
||||
const sharpe = (ret - portfolioConfig.riskFreeRate) / vol;
|
||||
|
||||
console.log(` Expected Return: ${(ret * 100).toFixed(2)}%`);
|
||||
console.log(` Volatility: ${(vol * 100).toFixed(2)}%`);
|
||||
console.log(` Sharpe Ratio: ${sharpe.toFixed(2)}`);
|
||||
}
|
||||
|
||||
// Run the example
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user