| |
| |
| |
| |
| |
|
|
| |
| let seed = 42; |
|
|
| export function seededRandom() { |
| seed = (seed * 16807 + 0) % 2147483647; |
| return (seed - 1) / 2147483646; |
| } |
|
|
| export function resetSeed(s = 42) { |
| seed = s; |
| } |
|
|
| |
| export const DEFAULT_TICKERS = [ |
| "AAPL","MSFT","GOOGL","AMZN","META","NVDA","TSLA","JPM","V","JNJ", |
| "PG","UNH","HD","MA","BAC","DIS","NFLX","KO","PFE","CVX", |
| "WMT","MRK","ABT","ADBE","NKE","PEP","T","VZ","PYPL","BRK", |
| ]; |
|
|
| export const DEFAULT_SECTORS = [ |
| "Tech","Tech","Tech","Tech","Tech","Tech","Tech","Finance","Finance","Health", |
| "Consumer","Health","Consumer","Finance","Finance","Consumer","Tech","Consumer", |
| "Health","Energy","Consumer","Health","Health","Tech","Consumer","Consumer", |
| "Telecom","Telecom","Tech","Finance", |
| ]; |
|
|
| |
| export function generateMarketData(nAssets, nDays, regime, seedVal, customTickerList) { |
| resetSeed(seedVal); |
| const regimeParams = { |
| bull: { drift: 0.0008, vol: 0.012, corrBase: 0.3 }, |
| bear: { drift: -0.0003, vol: 0.022, corrBase: 0.6 }, |
| volatile: { drift: 0.0002, vol: 0.028, corrBase: 0.45 }, |
| normal: { drift: 0.0004, vol: 0.015, corrBase: 0.35 }, |
| }; |
| const { drift, vol, corrBase } = regimeParams[regime]; |
| const rawNames = (Array.isArray(customTickerList) && customTickerList.length > 0) ? customTickerList : DEFAULT_TICKERS; |
| const names = []; |
| for (let i = 0; i < nAssets; i++) names.push(rawNames[i] || `A${i}`); |
| const sectors = DEFAULT_SECTORS; |
| const assets = []; |
| for (let i = 0; i < nAssets; i++) { |
| const assetDrift = drift + (seededRandom() - 0.4) * 0.001; |
| const assetVol = vol * (0.7 + seededRandom() * 0.6); |
| const returns = []; |
| for (let d = 0; d < nDays; d++) { |
| const r = assetDrift + assetVol * (seededRandom() + seededRandom() + seededRandom() - 1.5) * 0.816; |
| returns.push(r); |
| } |
| const annReturn = returns.reduce((a, b) => a + b, 0) / nDays * 252; |
| const annVol = Math.sqrt(returns.map(r => r * r).reduce((a, b) => a + b, 0) / nDays) * Math.sqrt(252); |
| assets.push({ name: names[i] || `A${i}`, sector: sectors[i] || "Other", returns, annReturn, annVol, sharpe: annReturn / (annVol || 1) }); |
| } |
| const corr = []; |
| for (let i = 0; i < nAssets; i++) { |
| corr[i] = []; |
| for (let j = 0; j < nAssets; j++) { |
| if (i === j) corr[i][j] = 1; |
| else if (j < i) corr[i][j] = corr[j][i]; |
| else { |
| const sameSector = sectors[i] === sectors[j] ? 0.2 : 0; |
| corr[i][j] = Math.max(-0.3, Math.min(0.95, corrBase + sameSector + (seededRandom() - 0.5) * 0.4)); |
| } |
| } |
| } |
| return { assets, corr, regime }; |
| } |
|
|
| |
| export function calculatePortfolioScore(weights, data, objective) { |
| const n = data.assets.length; |
| let portReturn = 0, portVar = 0; |
| for (let i = 0; i < n; i++) { |
| portReturn += weights[i] * data.assets[i].annReturn; |
| for (let j = 0; j < n; j++) { |
| portVar += weights[i] * weights[j] * data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; |
| } |
| } |
| const portVol = Math.sqrt(Math.max(0, portVar)); |
| const sharpe = portReturn / (portVol || 1); |
| const diversification = 1 / (weights.reduce((a, b) => a + b * b, 0) || 1); |
| switch (objective) { |
| case 'diversification': |
| return sharpe * 0.7 + diversification * 0.3; |
| case 'momentum': { |
| const momentumScore = weights.reduce((sum, w, i) => sum + w * data.assets[i].sharpe, 0); |
| return sharpe * 0.6 + momentumScore * 0.4; |
| } |
| case 'conservative': |
| return (sharpe > 0 ? sharpe : 0) * 0.8 + (1 / (portVol || 1)) * 0.2; |
| default: |
| return sharpe * 0.5 + diversification * 0.3 + (portReturn > 0 ? portReturn : 0) * 0.2; |
| } |
| } |
|
|
| export function calculateDiversificationRatio(weights, data) { |
| const portfolioVol = Math.sqrt(weights.reduce((variance, wi, i) => { |
| return variance + wi * wi * data.assets[i].annVol * data.assets[i].annVol + |
| weights.reduce((innerSum, wj, j) => { |
| if (i !== j) return innerSum + wi * wj * data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; |
| return innerSum; |
| }, 0); |
| }, 0)); |
| const weightedAvgVol = weights.reduce((sum, w, i) => sum + w * data.assets[i].annVol, 0); |
| return portfolioVol > 0 ? weightedAvgVol / portfolioVol : 1.0; |
| } |
|
|
| export function calculateInformationRatio(weights, data) { |
| const n = data.assets.length; |
| const equalWeights = new Array(n).fill(1 / n); |
| const portfolioReturn = weights.reduce((sum, w, i) => sum + w * data.assets[i].annReturn, 0); |
| const benchmarkReturn = equalWeights.reduce((sum, w, i) => sum + w * data.assets[i].annReturn, 0); |
| let trackingError = 0; |
| for (let i = 0; i < n; i++) { |
| for (let j = 0; j < n; j++) { |
| trackingError += (weights[i] - equalWeights[i]) * (weights[j] - equalWeights[j]) * |
| data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; |
| } |
| } |
| trackingError = Math.sqrt(Math.max(0, trackingError)); |
| const excessReturn = portfolioReturn - benchmarkReturn; |
| return trackingError > 0 ? excessReturn / trackingError : 0; |
| } |
|
|
| export function calculateAlpha(weights, data) { |
| const n = data.assets.length; |
| const portfolioReturn = weights.reduce((sum, w, i) => sum + w * data.assets[i].annReturn, 0); |
| const benchmarkReturn = data.assets.reduce((sum, asset) => sum + asset.annReturn, 0) / n; |
| return portfolioReturn - benchmarkReturn; |
| } |
|
|
| export function calculateBeta(weights, data) { |
| const n = data.assets.length; |
| const marketWeights = new Array(n).fill(1 / n); |
| let marketVar = 0; |
| for (let i = 0; i < n; i++) { |
| for (let j = 0; j < n; j++) { |
| marketVar += marketWeights[i] * marketWeights[j] * data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; |
| } |
| } |
| let cov = 0; |
| for (let i = 0; i < n; i++) { |
| for (let j = 0; j < n; j++) { |
| cov += weights[i] * marketWeights[j] * data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; |
| } |
| } |
| return marketVar > 0 ? cov / marketVar : 1.0; |
| } |
|
|
| export function calculateRiskContributions(weights, data) { |
| const n = data.assets.length; |
| const portfolioVol = Math.sqrt(weights.reduce((variance, wi, i) => { |
| return variance + wi * wi * data.assets[i].annVol * data.assets[i].annVol + |
| weights.reduce((innerSum, wj, j) => { |
| if (i !== j) return innerSum + wi * wj * data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; |
| return innerSum; |
| }, 0); |
| }, 0)); |
| const contributions = []; |
| for (let i = 0; i < n; i++) { |
| let marginalContribution = 0; |
| for (let j = 0; j < n; j++) { |
| marginalContribution += weights[j] * data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; |
| } |
| contributions.push(weights[i] * marginalContribution / (portfolioVol || 1)); |
| } |
| return contributions; |
| } |
|
|
| export function calculateSectorExposures(weights, data) { |
| const sectors = {}; |
| for (let i = 0; i < data.assets.length; i++) { |
| const sector = data.assets[i].sector || 'Unknown'; |
| sectors[sector] = (sectors[sector] || 0) + (weights[i] || 0); |
| } |
| return sectors; |
| } |
|
|
| export function calculateSharpe(weights, data) { |
| const n = data.assets.length; |
| let portReturn = 0, portVar = 0; |
| for (let i = 0; i < n; i++) { |
| portReturn += weights[i] * data.assets[i].annReturn; |
| for (let j = 0; j < n; j++) { |
| portVar += weights[i] * weights[j] * data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; |
| } |
| } |
| const portVol = Math.sqrt(Math.max(0, portVar)); |
| return portReturn / (portVol || 1); |
| } |
|
|
| |
| export function runEnhancedQSWOptimization(data, omega, evolutionTime, maxWeight, turnoverLimit, evolutionMethod = 'continuous', objective = 'balanced') { |
| const n = data.assets.length; |
| const weights = new Array(n).fill(0); |
| const sharpes = data.assets.map(a => Math.max(0.01, a.sharpe + 0.5)); |
| const totalSharpe = sharpes.reduce((a, b) => a + b, 0); |
|
|
| for (let i = 0; i < n; i++) { |
| let potential = sharpes[i] / totalSharpe; |
| let coupling = 0; |
| for (let j = 0; j < n; j++) { |
| if (i !== j) { |
| const diversBenefit = (1 - Math.abs(data.corr[i][j])) * sharpes[j] / totalSharpe; |
| coupling += diversBenefit; |
| } |
| } |
| weights[i] = (1 - omega) * potential + omega * coupling / (n - 1); |
| } |
|
|
| switch (evolutionMethod) { |
| case 'discrete': { |
| const dtSteps = evolutionTime * 2; |
| for (let step = 0; step < dtSteps; step++) { |
| const newWeights = [...weights]; |
| for (let i = 0; i < n; i++) { |
| let neighborSum = 0; |
| let neighborCount = 0; |
| for (let j = 0; j < n; j++) { |
| if (i !== j && Math.abs(data.corr[i][j]) > 0.1) { neighborSum += weights[j]; neighborCount++; } |
| } |
| if (neighborCount > 0) newWeights[i] = (weights[i] + neighborSum / neighborCount) / 2; |
| } |
| for (let i = 0; i < n; i++) weights[i] = newWeights[i]; |
| } |
| break; |
| } |
| case 'decoherent': { |
| const decoherenceRate = 0.15; |
| for (let i = 0; i < n; i++) weights[i] = (1 - decoherenceRate) * weights[i] + decoherenceRate * (1 / n); |
| break; |
| } |
| case 'adiabatic': { |
| const adiabaticSteps = evolutionTime * 3; |
| for (let step = 0; step < adiabaticSteps; step++) { |
| const s = step / adiabaticSteps; |
| const currentOmega = omega * s; |
| for (let i = 0; i < n; i++) { |
| let potential = sharpes[i] / totalSharpe; |
| let coupling = 0; |
| for (let j = 0; j < n; j++) { |
| if (i !== j) { |
| const diversBenefit = (1 - Math.abs(data.corr[i][j])) * sharpes[j] / totalSharpe; |
| coupling += diversBenefit; |
| } |
| } |
| weights[i] = (1 - currentOmega) * potential + currentOmega * coupling / (n - 1); |
| } |
| } |
| break; |
| } |
| case 'variational': { |
| let bestWeights = [...weights]; |
| let bestScore = calculatePortfolioScore(weights, data, objective); |
| for (let trial = 0; trial < 5; trial++) { |
| const trialOmega = omega * (0.8 + seededRandom() * 0.4); |
| const trialWeights = new Array(n).fill(0); |
| for (let i = 0; i < n; i++) { |
| let potential = sharpes[i] / totalSharpe; |
| let coupling = 0; |
| for (let j = 0; j < n; j++) { |
| if (i !== j) { |
| const diversBenefit = (1 - Math.abs(data.corr[i][j])) * sharpes[j] / totalSharpe; |
| coupling += diversBenefit; |
| } |
| } |
| trialWeights[i] = (1 - trialOmega) * potential + trialOmega * coupling / (n - 1); |
| } |
| for (let i = 0; i < n; i++) trialWeights[i] = Math.min(trialWeights[i], maxWeight); |
| for (let i = 0; i < n; i++) if (trialWeights[i] < 0.005) trialWeights[i] = 0; |
| const trialSum = trialWeights.reduce((a, b) => a + b, 0); |
| if (trialSum > 0) for (let i = 0; i < n; i++) trialWeights[i] /= trialSum; |
| const trialScore = calculatePortfolioScore(trialWeights, data, objective); |
| if (trialScore > bestScore) { bestScore = trialScore; bestWeights = [...trialWeights]; } |
| } |
| for (let i = 0; i < n; i++) weights[i] = bestWeights[i]; |
| break; |
| } |
| default: { |
| const smoothFactor = Math.exp(-evolutionTime / 50); |
| const equalW = 1 / n; |
| for (let i = 0; i < n; i++) weights[i] = weights[i] * (1 - smoothFactor) + equalW * smoothFactor; |
| } |
| } |
|
|
| switch (objective) { |
| case 'diversification': |
| for (let i = 0; i < n; i++) weights[i] = Math.min(weights[i], maxWeight * 0.8); |
| break; |
| case 'momentum': |
| for (let i = 0; i < n; i++) weights[i] = Math.min(weights[i], maxWeight * 1.2); |
| break; |
| case 'conservative': |
| for (let i = 0; i < n; i++) weights[i] = Math.min(weights[i], maxWeight * 0.7); |
| break; |
| default: |
| for (let i = 0; i < n; i++) weights[i] = Math.min(weights[i], maxWeight); |
| } |
|
|
| for (let i = 0; i < n; i++) if (weights[i] < 0.005) weights[i] = 0; |
| const sum = weights.reduce((a, b) => a + b, 0); |
| if (sum === 0) { for (let i = 0; i < n; i++) weights[i] = 1 / n; } |
| else { for (let i = 0; i < n; i++) weights[i] /= sum; } |
|
|
| let portReturn = 0, portVar = 0; |
| for (let i = 0; i < n; i++) { |
| portReturn += weights[i] * data.assets[i].annReturn; |
| for (let j = 0; j < n; j++) { |
| portVar += weights[i] * weights[j] * data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; |
| } |
| } |
| const portVol = Math.sqrt(Math.max(0, portVar)); |
| const sharpe = portReturn / (portVol || 1); |
|
|
| return { |
| weights, portReturn, portVol, sharpe, |
| nActive: weights.filter(w => w > 0.005).length, |
| diversificationRatio: calculateDiversificationRatio(weights, data), |
| informationRatio: calculateInformationRatio(weights, data), |
| alpha: calculateAlpha(weights, data), |
| beta: calculateBeta(weights, data), |
| riskContributions: calculateRiskContributions(weights, data), |
| sectorExposures: calculateSectorExposures(weights, data), |
| }; |
| } |
|
|
| |
| export function runQuantumAnnealingOptimization(data, maxWeight) { |
| const n = data.assets.length; |
| const weights = new Array(n).fill(0); |
| for (let i = 0; i < n; i++) weights[i] = seededRandom(); |
| const sum = weights.reduce((a, b) => a + b, 0); |
| for (let i = 0; i < n; i++) weights[i] /= sum; |
|
|
| const iterations = 100; |
| let currentSharpe = calculateSharpe(weights, data); |
| for (let iter = 0; iter < iterations; iter++) { |
| const neighborWeights = [...weights]; |
| const temp = 100 * Math.exp(-iter / 20); |
| for (let i = 0; i < n; i++) { |
| neighborWeights[i] += (seededRandom() - 0.5) * 0.1; |
| neighborWeights[i] = Math.max(0, Math.min(maxWeight, neighborWeights[i])); |
| } |
| const neighborSum = neighborWeights.reduce((a, b) => a + b, 0); |
| for (let i = 0; i < n; i++) neighborWeights[i] /= neighborSum; |
| const neighborSharpe = calculateSharpe(neighborWeights, data); |
| if (neighborSharpe > currentSharpe || Math.exp(-(currentSharpe - neighborSharpe) / temp) > seededRandom()) { |
| for (let i = 0; i < n; i++) weights[i] = neighborWeights[i]; |
| currentSharpe = neighborSharpe; |
| } |
| } |
|
|
| for (let i = 0; i < n; i++) weights[i] = Math.min(weights[i], maxWeight); |
| for (let i = 0; i < n; i++) if (weights[i] < 0.005) weights[i] = 0; |
| const finalSum = weights.reduce((a, b) => a + b, 0); |
| if (finalSum > 0) for (let i = 0; i < n; i++) weights[i] /= finalSum; |
|
|
| let portReturn = 0, portVar = 0; |
| for (let i = 0; i < n; i++) { |
| portReturn += weights[i] * data.assets[i].annReturn; |
| for (let j = 0; j < n; j++) { |
| portVar += weights[i] * weights[j] * data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; |
| } |
| } |
| const portVol = Math.sqrt(Math.max(0, portVar)); |
| const sharpe = portReturn / (portVol || 1); |
|
|
| return { |
| weights, portReturn, portVol, sharpe, |
| nActive: weights.filter(w => w > 0.005).length, |
| diversificationRatio: calculateDiversificationRatio(weights, data), |
| informationRatio: calculateInformationRatio(weights, data), |
| alpha: calculateAlpha(weights, data), |
| beta: calculateBeta(weights, data), |
| riskContributions: calculateRiskContributions(weights, data), |
| sectorExposures: calculateSectorExposures(weights, data), |
| }; |
| } |
|
|
| |
| function computeHRPWeights(data) { |
| const n = data.assets.length; |
| if (n <= 1) return new Array(n).fill(1); |
|
|
| const dist = []; |
| for (let i = 0; i < n; i++) { |
| dist[i] = []; |
| for (let j = 0; j < n; j++) { |
| dist[i][j] = Math.sqrt(0.5 * (1 - (data.corr[i]?.[j] ?? (i === j ? 1 : 0)))); |
| } |
| } |
|
|
| const clusters = Array.from({ length: n }, (_, i) => [i]); |
| const active = new Array(n).fill(true); |
|
|
| while (active.filter(Boolean).length > 1) { |
| let minDist = Infinity, mergeA = -1, mergeB = -1; |
| for (let i = 0; i < clusters.length; i++) { |
| if (!active[i]) continue; |
| for (let j = i + 1; j < clusters.length; j++) { |
| if (!active[j]) continue; |
| let d = Infinity; |
| for (const a of clusters[i]) { |
| for (const b of clusters[j]) { |
| d = Math.min(d, dist[a][b]); |
| } |
| } |
| if (d < minDist) { minDist = d; mergeA = i; mergeB = j; } |
| } |
| } |
| if (mergeA < 0) break; |
| clusters.push([...clusters[mergeA], ...clusters[mergeB]]); |
| active.push(true); |
| active[mergeA] = false; |
| active[mergeB] = false; |
| } |
|
|
| const sortedIndices = clusters[clusters.length - 1] || Array.from({ length: n }, (_, i) => i); |
|
|
| function clusterVariance(indices) { |
| if (indices.length === 0) return 1; |
| const invV = indices.map(i => 1 / (data.assets[i].annVol * data.assets[i].annVol || 1)); |
| const ivSum = invV.reduce((a, b) => a + b, 0) || 1; |
| const w = invV.map(v => v / ivSum); |
| let variance = 0; |
| for (let ii = 0; ii < indices.length; ii++) { |
| for (let jj = 0; jj < indices.length; jj++) { |
| const i = indices[ii], j = indices[jj]; |
| variance += w[ii] * w[jj] * (data.corr[i]?.[j] ?? (i === j ? 1 : 0)) * data.assets[i].annVol * data.assets[j].annVol; |
| } |
| } |
| return Math.max(1e-10, variance); |
| } |
|
|
| const weights = new Array(n).fill(1); |
|
|
| function recursiveBisect(items) { |
| if (items.length <= 1) return; |
| const mid = Math.floor(items.length / 2); |
| const left = items.slice(0, mid); |
| const right = items.slice(mid); |
| const vL = clusterVariance(left); |
| const vR = clusterVariance(right); |
| const alpha = 1 - vL / (vL + vR); |
| for (const i of left) weights[i] *= alpha; |
| for (const i of right) weights[i] *= (1 - alpha); |
| recursiveBisect(left); |
| recursiveBisect(right); |
| } |
|
|
| recursiveBisect(sortedIndices); |
|
|
| const sum = weights.reduce((a, b) => a + b, 0) || 1; |
| return weights.map(w => w / sum); |
| } |
|
|
| |
| export function runBenchmarks(data) { |
| const n = data.assets.length; |
| const calc = (w) => { |
| let r = 0, v = 0; |
| for (let i = 0; i < n; i++) { r += w[i] * data.assets[i].annReturn; for (let j = 0; j < n; j++) v += w[i] * w[j] * data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; } |
| const vol = Math.sqrt(Math.max(0, v)); |
| return { weights: w, portReturn: r, portVol: vol, sharpe: r / (vol || 1) }; |
| }; |
| const ew = new Array(n).fill(1 / n); |
| const invVol = data.assets.map(a => 1 / (a.annVol || 1)); |
| const ivSum = invVol.reduce((a, b) => a + b, 0); |
| const mv = invVol.map(v => v / ivSum); |
| const riskBudget = data.assets.map(a => 1 / (a.annVol * a.annVol || 1)); |
| const rbSum = riskBudget.reduce((a, b) => a + b, 0); |
| const rp = riskBudget.map(v => v / rbSum); |
| const sharpeW = data.assets.map(a => Math.max(0, a.sharpe)); |
| const swSum = sharpeW.reduce((a, b) => a + b, 0) || 1; |
| const ms = sharpeW.map(v => v / swSum); |
| const hrpW = computeHRPWeights(data); |
|
|
| return { |
| equalWeight: { name: "Equal Weight", ...calc(ew) }, |
| minVariance: { name: "Min Variance", ...calc(mv) }, |
| riskParity: { name: "Risk Parity", ...calc(rp) }, |
| maxSharpe: { name: "Max Sharpe", ...calc(ms) }, |
| hrp: { name: "HRP", ...calc(hrpW) }, |
| }; |
| } |
|
|
| |
| export function runOptimisation(data, opts = {}) { |
| const { objective = "hybrid", K, KScreen, KSelect, wMax = 0.20 } = opts; |
| const n = data.assets.length; |
| const maxWeight = wMax; |
|
|
| switch (objective) { |
| case "equal_weight": { |
| const w = new Array(n).fill(1 / n); |
| return _metrics(w, data, null); |
| } |
| case "markowitz": |
| case "min_variance": |
| case "target_return": { |
| const r = runEnhancedQSWOptimization(data, 0.3, 10, maxWeight, 0.2, "continuous", objective === "markowitz" ? "balanced" : "conservative"); |
| return _metrics(r.weights, data, null); |
| } |
| case "hrp": { |
| const w = computeHRPWeights(data); |
| return _metrics(w, data, null); |
| } |
| case "qubo_sa": { |
| const k = K || Math.min(Math.ceil(n * 0.4), 8); |
| const selected = data.assets.map((a, i) => ({ i, score: a.sharpe })).sort((a, b) => b.score - a.score).slice(0, k).map(x => x.i); |
| const w = new Array(n).fill(0); |
| selected.forEach(i => { w[i] = 1 / k; }); |
| return _metrics(w, data, { stage2_selected_idx: selected, stage2_qubo_obj: 0, stage1_screened_count: n, stage3_sharpe: 0 }); |
| } |
| case "vqe": { |
| const r = runEnhancedQSWOptimization(data, 0.25, 8, maxWeight, 0.2, "continuous", "balanced"); |
| return _metrics(r.weights, data, null); |
| } |
| case "hybrid": |
| default: { |
| const kScr = KScreen || Math.min(Math.ceil(n * 0.6), 15); |
| const kSel = KSelect || Math.min(Math.ceil(kScr * 0.5), 5); |
| const screened = data.assets.map((a, i) => ({ i, score: a.sharpe + (1 - a.annVol) * 0.5 })).sort((a, b) => b.score - a.score).slice(0, kScr); |
| const selected = screened.slice(0, kSel); |
| const w = new Array(n).fill(0); |
| selected.forEach(s => { w[s.i] = 1 / kSel; }); |
| const selectedNames = selected.map(s => data.assets[s.i].name); |
| const r = _metrics(w, data, { |
| stage1_screened_count: kScr, |
| stage2_selected_idx: selected.map(s => s.i), |
| stage2_selected_names: selectedNames, |
| stage2_qubo_obj: 0, |
| stage3_sharpe: 0, |
| }); |
| if (r.stage_info) r.stage_info.stage3_sharpe = r.sharpe; |
| return r; |
| } |
| } |
| } |
|
|
| function _metrics(weights, data, stageInfo) { |
| const n = data.assets.length; |
| let portReturn = 0, portVar = 0; |
| for (let i = 0; i < n; i++) { |
| portReturn += weights[i] * data.assets[i].annReturn; |
| for (let j = 0; j < n; j++) { |
| portVar += weights[i] * weights[j] * data.corr[i][j] * data.assets[i].annVol * data.assets[j].annVol; |
| } |
| } |
| const portVol = Math.sqrt(Math.max(0, portVar)); |
| return { weights, portReturn, portVol, sharpe: portReturn / (portVol || 1), nActive: weights.filter(w => w > 0.005).length, stage_info: stageInfo }; |
| } |
|
|
| |
| |
| |
| |
| |
| export function portfolioMetricsFromWeights(weights, data) { |
| const w = weights.map((x) => Number(x) || 0); |
| return _metrics(w, data, null); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function clipNormalizeWeights(weights, wMin, wMax) { |
| const n = weights.length; |
| if (n === 0) return []; |
| let w = weights.map((x) => Math.max(wMin, Math.min(wMax, Number(x) || 0))); |
| let s = w.reduce((a, b) => a + b, 0); |
| if (s <= 1e-15) { |
| w = new Array(n).fill(1 / n); |
| w = w.map((x) => Math.max(wMin, Math.min(wMax, x))); |
| s = w.reduce((a, b) => a + b, 0); |
| } |
| if (s > 0) w = w.map((x) => x / s); |
| w = w.map((x) => Math.max(wMin, Math.min(wMax, x))); |
| s = w.reduce((a, b) => a + b, 0); |
| if (s > 0) w = w.map((x) => x / s); |
| return w; |
| } |
|
|
| export function computeHRPWeightsArr(data) { |
| return computeHRPWeights(data); |
| } |
|
|
| |
| export function simulateEquityCurve(data, weights, nDays) { |
| const curve = [{ day: 0, value: 100 }]; |
| let val = 100; |
| for (let d = 0; d < Math.min(nDays, data.assets[0]?.returns.length || 0); d++) { |
| let dayReturn = 0; |
| for (let i = 0; i < weights.length; i++) dayReturn += weights[i] * (data.assets[i]?.returns[d] || 0); |
| val *= (1 + dayReturn); |
| if (d % 5 === 0) curve.push({ day: d + 1, value: val }); |
| } |
| return curve; |
| } |
|
|
| |
| |
| |
| |
| export function simulatePerAssetEquity(data, weights, nDays, notional = 100000) { |
| const n = weights.length; |
| const maxD = Math.min(nDays, data.assets[0]?.returns.length || 0); |
| const positions = weights.map((w) => w * notional); |
| const snapDays = [0]; |
| const totalCurve = [{ day: 0, value: notional }]; |
|
|
| for (let d = 0; d < maxD; d++) { |
| for (let i = 0; i < n; i++) { |
| positions[i] *= 1 + (data.assets[i]?.returns[d] || 0); |
| } |
| if (d % 5 === 0) { |
| const day = d + 1; |
| snapDays.push(day); |
| totalCurve.push({ day, value: positions.reduce((a, b) => a + b, 0) }); |
| } |
| } |
|
|
| const finalTotal = positions.reduce((a, b) => a + b, 0); |
| const finalPositions = data.assets.map((a, i) => ({ |
| name: a.name, |
| sector: a.sector, |
| initAlloc: weights[i] * notional, |
| currentValue: positions[i], |
| pnl: positions[i] - weights[i] * notional, |
| pnlPct: weights[i] > 0 ? (positions[i] / (weights[i] * notional) - 1) * 100 : 0, |
| weight: weights[i], |
| })); |
|
|
| return { |
| total: totalCurve, |
| finalPositions, |
| summary: { |
| notional, |
| currentValue: finalTotal, |
| totalPnl: finalTotal - notional, |
| totalReturnPct: (finalTotal / notional - 1) * 100, |
| }, |
| }; |
| } |
|
|
| |
| export function computeVaR(data, weights, confidence) { |
| const n = weights.length; |
| const nSim = 2000; |
| const losses = []; |
| resetSeed(123); |
| for (let s = 0; s < nSim; s++) { |
| let portReturn = 0; |
| for (let i = 0; i < n; i++) { |
| const dayIdx = Math.floor(seededRandom() * (data.assets[i]?.returns.length || 1)); |
| portReturn += weights[i] * (data.assets[i]?.returns[dayIdx] || 0); |
| } |
| losses.push(-portReturn); |
| } |
| losses.sort((a, b) => a - b); |
| const varIdx = Math.floor(nSim * confidence); |
| const var95 = losses[varIdx] || 0; |
| const cvar = losses.slice(varIdx).reduce((a, b) => a + b, 0) / (nSim - varIdx || 1); |
| return { var95: var95 * 100, cvar: cvar * 100 }; |
| } |
|
|