832 lines
25 KiB
JavaScript
832 lines
25 KiB
JavaScript
/**
|
|
* Quantum-Inspired Portfolio Optimization
|
|
*
|
|
* EXOTIC: Quantum annealing and QAOA for portfolio selection
|
|
*
|
|
* Uses @neural-trader/portfolio with RuVector for:
|
|
* - Quantum Approximate Optimization Algorithm (QAOA) simulation
|
|
* - Simulated quantum annealing for combinatorial optimization
|
|
* - Qubit encoding of portfolio weights
|
|
* - Quantum interference for exploring solution space
|
|
*
|
|
* Classical simulation of quantum concepts for optimization
|
|
* problems that are NP-hard classically.
|
|
*/
|
|
|
|
// Quantum optimization configuration
|
|
const quantumConfig = {
|
|
// QAOA parameters
|
|
qaoa: {
|
|
layers: 3, // Number of QAOA layers (p)
|
|
shots: 1000, // Measurement samples
|
|
optimizer: 'cobyla', // Classical optimizer for angles
|
|
maxIterations: 100
|
|
},
|
|
|
|
// Annealing parameters
|
|
annealing: {
|
|
initialTemp: 100,
|
|
finalTemp: 0.01,
|
|
coolingRate: 0.99,
|
|
sweeps: 1000
|
|
},
|
|
|
|
// Portfolio constraints
|
|
portfolio: {
|
|
numAssets: 10,
|
|
minWeight: 0.0,
|
|
maxWeight: 0.3,
|
|
targetReturn: 0.10,
|
|
riskAversion: 2.0,
|
|
cardinalityConstraint: 5 // Max assets in portfolio
|
|
},
|
|
|
|
// Qubit encoding
|
|
encoding: {
|
|
bitsPerWeight: 4, // Weight precision: 2^4 = 16 levels
|
|
penaltyWeight: 100 // Constraint violation penalty
|
|
}
|
|
};
|
|
|
|
// Object pool for Complex numbers (reduces GC pressure)
|
|
class ComplexPool {
|
|
constructor(initialSize = 1024) {
|
|
this.pool = [];
|
|
this.index = 0;
|
|
for (let i = 0; i < initialSize; i++) {
|
|
this.pool.push(new Complex(0, 0));
|
|
}
|
|
}
|
|
|
|
acquire(real = 0, imag = 0) {
|
|
if (this.index < this.pool.length) {
|
|
const c = this.pool[this.index++];
|
|
c.real = real;
|
|
c.imag = imag;
|
|
return c;
|
|
}
|
|
// Expand pool if needed
|
|
const c = new Complex(real, imag);
|
|
this.pool.push(c);
|
|
this.index++;
|
|
return c;
|
|
}
|
|
|
|
reset() {
|
|
this.index = 0;
|
|
}
|
|
}
|
|
|
|
// Global pool instance for reuse
|
|
const complexPool = new ComplexPool(4096);
|
|
|
|
// Complex number class for quantum states
|
|
class Complex {
|
|
constructor(real, imag = 0) {
|
|
this.real = real;
|
|
this.imag = imag;
|
|
}
|
|
|
|
add(other) {
|
|
return new Complex(this.real + other.real, this.imag + other.imag);
|
|
}
|
|
|
|
// In-place add (avoids allocation)
|
|
addInPlace(other) {
|
|
this.real += other.real;
|
|
this.imag += other.imag;
|
|
return this;
|
|
}
|
|
|
|
multiply(other) {
|
|
return new Complex(
|
|
this.real * other.real - this.imag * other.imag,
|
|
this.real * other.imag + this.imag * other.real
|
|
);
|
|
}
|
|
|
|
// In-place multiply (avoids allocation)
|
|
multiplyInPlace(other) {
|
|
const newReal = this.real * other.real - this.imag * other.imag;
|
|
const newImag = this.real * other.imag + this.imag * other.real;
|
|
this.real = newReal;
|
|
this.imag = newImag;
|
|
return this;
|
|
}
|
|
|
|
scale(s) {
|
|
return new Complex(this.real * s, this.imag * s);
|
|
}
|
|
|
|
// In-place scale (avoids allocation)
|
|
scaleInPlace(s) {
|
|
this.real *= s;
|
|
this.imag *= s;
|
|
return this;
|
|
}
|
|
|
|
magnitude() {
|
|
return Math.sqrt(this.real * this.real + this.imag * this.imag);
|
|
}
|
|
|
|
magnitudeSq() {
|
|
return this.real * this.real + this.imag * this.imag;
|
|
}
|
|
|
|
static exp(theta) {
|
|
return new Complex(Math.cos(theta), Math.sin(theta));
|
|
}
|
|
}
|
|
|
|
// Quantum State (simplified simulation)
|
|
class QuantumState {
|
|
constructor(numQubits) {
|
|
this.numQubits = numQubits;
|
|
this.dim = Math.pow(2, numQubits);
|
|
this.amplitudes = new Array(this.dim).fill(null).map(() => new Complex(0));
|
|
this.amplitudes[0] = new Complex(1); // Initialize to |0...0⟩
|
|
}
|
|
|
|
// Create uniform superposition (Hadamard on all qubits)
|
|
hadamardAll() {
|
|
const newAmps = new Array(this.dim).fill(null).map(() => new Complex(0));
|
|
const norm = 1 / Math.sqrt(this.dim);
|
|
|
|
for (let i = 0; i < this.dim; i++) {
|
|
newAmps[i] = new Complex(norm);
|
|
}
|
|
|
|
this.amplitudes = newAmps;
|
|
}
|
|
|
|
// Apply cost Hamiltonian phase (problem encoding)
|
|
applyCostPhase(gamma, costFunction) {
|
|
for (let i = 0; i < this.dim; i++) {
|
|
const cost = costFunction(i);
|
|
const phase = Complex.exp(-gamma * cost);
|
|
this.amplitudes[i] = this.amplitudes[i].multiply(phase);
|
|
}
|
|
}
|
|
|
|
// Apply mixer Hamiltonian (exploration)
|
|
// Implements exp(-i * beta * sum_j X_j) where X_j is Pauli-X on qubit j
|
|
applyMixerPhase(beta) {
|
|
const cos = Math.cos(beta);
|
|
const sin = Math.sin(beta);
|
|
|
|
// Apply Rx(2*beta) to each qubit individually
|
|
// Rx(theta) = cos(theta/2)*I - i*sin(theta/2)*X
|
|
for (let q = 0; q < this.numQubits; q++) {
|
|
const newAmps = new Array(this.dim).fill(null).map(() => new Complex(0));
|
|
|
|
for (let i = 0; i < this.dim; i++) {
|
|
const neighbor = i ^ (1 << q); // Flip qubit q
|
|
|
|
// |i⟩ -> cos(beta)|i⟩ - i*sin(beta)|neighbor⟩
|
|
newAmps[i] = newAmps[i].add(this.amplitudes[i].scale(cos));
|
|
newAmps[i] = newAmps[i].add(
|
|
new Complex(0, -sin).multiply(this.amplitudes[neighbor])
|
|
);
|
|
}
|
|
|
|
// Update amplitudes after each qubit rotation
|
|
for (let i = 0; i < this.dim; i++) {
|
|
this.amplitudes[i] = newAmps[i];
|
|
}
|
|
}
|
|
|
|
// Normalize to handle numerical errors
|
|
let norm = 0;
|
|
for (const amp of this.amplitudes) {
|
|
norm += amp.magnitude() ** 2;
|
|
}
|
|
norm = Math.sqrt(norm);
|
|
|
|
// Guard against division by zero
|
|
if (norm > 1e-10) {
|
|
for (let i = 0; i < this.dim; i++) {
|
|
this.amplitudes[i] = this.amplitudes[i].scale(1 / norm);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Measure (sample from probability distribution)
|
|
measure() {
|
|
const probabilities = this.amplitudes.map(a => a.magnitude() ** 2);
|
|
|
|
// Normalize probabilities with guard against zero total
|
|
const total = probabilities.reduce((a, b) => a + b, 0);
|
|
if (total < 1e-10) {
|
|
// Fallback to uniform distribution
|
|
return Math.floor(Math.random() * this.dim);
|
|
}
|
|
const normalized = probabilities.map(p => p / total);
|
|
|
|
// Sample
|
|
const r = Math.random();
|
|
let cumulative = 0;
|
|
|
|
for (let i = 0; i < this.dim; i++) {
|
|
cumulative += normalized[i];
|
|
if (r < cumulative) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return this.dim - 1;
|
|
}
|
|
|
|
// Get probability distribution
|
|
getProbabilities() {
|
|
const probs = this.amplitudes.map(a => a.magnitude() ** 2);
|
|
const total = probs.reduce((a, b) => a + b, 0);
|
|
return probs.map(p => p / total);
|
|
}
|
|
}
|
|
|
|
// QAOA Optimizer
|
|
class QAOAOptimizer {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this.bestSolution = null;
|
|
this.bestCost = Infinity;
|
|
this.history = [];
|
|
}
|
|
|
|
// Define cost function (portfolio objective)
|
|
createCostFunction(expectedReturns, covarianceMatrix, riskAversion) {
|
|
return (bitstring) => {
|
|
const weights = this.decodeWeights(bitstring);
|
|
|
|
// Expected return
|
|
const expectedReturn = weights.reduce((sum, w, i) => sum + w * expectedReturns[i], 0);
|
|
|
|
// Portfolio variance
|
|
let variance = 0;
|
|
for (let i = 0; i < weights.length; i++) {
|
|
for (let j = 0; j < weights.length; j++) {
|
|
variance += weights[i] * weights[j] * covarianceMatrix[i][j];
|
|
}
|
|
}
|
|
|
|
// Mean-variance objective (maximize return, minimize variance)
|
|
// Cost = -return + riskAversion * variance
|
|
let cost = -expectedReturn + riskAversion * variance;
|
|
|
|
// Penalty for constraint violations
|
|
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
if (Math.abs(totalWeight - 1.0) > 0.1) {
|
|
cost += this.config.encoding.penaltyWeight * (totalWeight - 1.0) ** 2;
|
|
}
|
|
|
|
// Cardinality constraint penalty
|
|
const numAssets = weights.filter(w => w > 0.01).length;
|
|
if (numAssets > this.config.portfolio.cardinalityConstraint) {
|
|
cost += this.config.encoding.penaltyWeight * (numAssets - this.config.portfolio.cardinalityConstraint);
|
|
}
|
|
|
|
return cost;
|
|
};
|
|
}
|
|
|
|
// Decode bitstring to portfolio weights
|
|
decodeWeights(bitstring) {
|
|
const numAssets = this.config.portfolio.numAssets;
|
|
const bitsPerWeight = this.config.encoding.bitsPerWeight;
|
|
const maxLevel = Math.pow(2, bitsPerWeight) - 1;
|
|
|
|
const weights = [];
|
|
|
|
for (let i = 0; i < numAssets; i++) {
|
|
let value = 0;
|
|
for (let b = 0; b < bitsPerWeight; b++) {
|
|
const bitIndex = i * bitsPerWeight + b;
|
|
if (bitstring & (1 << bitIndex)) {
|
|
value += Math.pow(2, b);
|
|
}
|
|
}
|
|
|
|
// Normalize to weight range
|
|
const weight = (value / maxLevel) * this.config.portfolio.maxWeight;
|
|
weights.push(weight);
|
|
}
|
|
|
|
// Normalize to sum to 1
|
|
const total = weights.reduce((a, b) => a + b, 0);
|
|
if (total > 0) {
|
|
return weights.map(w => w / total);
|
|
}
|
|
|
|
return weights;
|
|
}
|
|
|
|
// Run QAOA
|
|
runQAOA(expectedReturns, covarianceMatrix) {
|
|
const numQubits = this.config.portfolio.numAssets * this.config.encoding.bitsPerWeight;
|
|
const costFunction = this.createCostFunction(
|
|
expectedReturns,
|
|
covarianceMatrix,
|
|
this.config.portfolio.riskAversion
|
|
);
|
|
|
|
// Initialize angles
|
|
let gammas = new Array(this.config.qaoa.layers).fill(0.5);
|
|
let betas = new Array(this.config.qaoa.layers).fill(0.3);
|
|
|
|
// Classical optimization loop (simplified gradient-free)
|
|
for (let iter = 0; iter < this.config.qaoa.maxIterations; iter++) {
|
|
const result = this.evaluateQAOA(numQubits, gammas, betas, costFunction);
|
|
|
|
if (result.avgCost < this.bestCost) {
|
|
this.bestCost = result.avgCost;
|
|
this.bestSolution = result.bestBitstring;
|
|
}
|
|
|
|
this.history.push({
|
|
iteration: iter,
|
|
avgCost: result.avgCost,
|
|
bestCost: this.bestCost
|
|
});
|
|
|
|
// Simple parameter update (gradient-free)
|
|
for (let l = 0; l < this.config.qaoa.layers; l++) {
|
|
gammas[l] += (Math.random() - 0.5) * 0.1 * (1 - iter / this.config.qaoa.maxIterations);
|
|
betas[l] += (Math.random() - 0.5) * 0.1 * (1 - iter / this.config.qaoa.maxIterations);
|
|
}
|
|
}
|
|
|
|
return {
|
|
bestBitstring: this.bestSolution,
|
|
bestWeights: this.decodeWeights(this.bestSolution),
|
|
bestCost: this.bestCost,
|
|
history: this.history
|
|
};
|
|
}
|
|
|
|
// Evaluate QAOA for given angles
|
|
evaluateQAOA(numQubits, gammas, betas, costFunction) {
|
|
// Use smaller qubit count for simulation
|
|
const effectiveQubits = Math.min(numQubits, 12);
|
|
|
|
const state = new QuantumState(effectiveQubits);
|
|
state.hadamardAll();
|
|
|
|
// Apply QAOA layers
|
|
for (let l = 0; l < this.config.qaoa.layers; l++) {
|
|
state.applyCostPhase(gammas[l], costFunction);
|
|
state.applyMixerPhase(betas[l]);
|
|
}
|
|
|
|
// Sample solutions
|
|
let totalCost = 0;
|
|
let bestCost = Infinity;
|
|
let bestBitstring = 0;
|
|
|
|
for (let shot = 0; shot < this.config.qaoa.shots; shot++) {
|
|
const measured = state.measure();
|
|
const cost = costFunction(measured);
|
|
totalCost += cost;
|
|
|
|
if (cost < bestCost) {
|
|
bestCost = cost;
|
|
bestBitstring = measured;
|
|
}
|
|
}
|
|
|
|
return {
|
|
avgCost: totalCost / this.config.qaoa.shots,
|
|
bestCost,
|
|
bestBitstring
|
|
};
|
|
}
|
|
}
|
|
|
|
// Simulated Quantum Annealing
|
|
class QuantumAnnealer {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this.bestSolution = null;
|
|
this.bestEnergy = Infinity;
|
|
this.history = [];
|
|
}
|
|
|
|
// QUBO formulation for portfolio optimization
|
|
createQUBOMatrix(expectedReturns, covarianceMatrix, riskAversion) {
|
|
const n = expectedReturns.length;
|
|
const Q = Array(n).fill(null).map(() => Array(n).fill(0));
|
|
|
|
// Linear terms (returns)
|
|
for (let i = 0; i < n; i++) {
|
|
Q[i][i] = -expectedReturns[i];
|
|
}
|
|
|
|
// Quadratic terms (covariance)
|
|
for (let i = 0; i < n; i++) {
|
|
for (let j = 0; j < n; j++) {
|
|
Q[i][j] += riskAversion * covarianceMatrix[i][j];
|
|
}
|
|
}
|
|
|
|
return Q;
|
|
}
|
|
|
|
// Calculate QUBO energy
|
|
calculateEnergy(Q, solution) {
|
|
let energy = 0;
|
|
const n = Q.length;
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
for (let j = 0; j < n; j++) {
|
|
energy += Q[i][j] * solution[i] * solution[j];
|
|
}
|
|
}
|
|
|
|
// Constraint: sum of weights should be close to 1
|
|
const totalWeight = solution.reduce((a, b) => a + b, 0);
|
|
const constraint = this.config.encoding.penaltyWeight * (totalWeight / n - 1) ** 2;
|
|
|
|
return energy + constraint;
|
|
}
|
|
|
|
// Run simulated quantum annealing
|
|
runAnnealing(expectedReturns, covarianceMatrix) {
|
|
const Q = this.createQUBOMatrix(
|
|
expectedReturns,
|
|
covarianceMatrix,
|
|
this.config.portfolio.riskAversion
|
|
);
|
|
const n = expectedReturns.length;
|
|
|
|
// Initialize random binary solution
|
|
let solution = Array(n).fill(0).map(() => Math.random() < 0.5 ? 1 : 0);
|
|
let energy = this.calculateEnergy(Q, solution);
|
|
|
|
this.bestSolution = [...solution];
|
|
this.bestEnergy = energy;
|
|
|
|
let temp = this.config.annealing.initialTemp;
|
|
|
|
for (let sweep = 0; sweep < this.config.annealing.sweeps; sweep++) {
|
|
// Quantum tunneling probability (higher at low temps in quantum annealing)
|
|
const tunnelProb = Math.exp(-sweep / this.config.annealing.sweeps);
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
// Propose flip
|
|
const newSolution = [...solution];
|
|
newSolution[i] = 1 - newSolution[i];
|
|
|
|
// Also consider tunneling (flip multiple bits)
|
|
if (Math.random() < tunnelProb * 0.1) {
|
|
const j = Math.floor(Math.random() * n);
|
|
if (j !== i) newSolution[j] = 1 - newSolution[j];
|
|
}
|
|
|
|
const newEnergy = this.calculateEnergy(Q, newSolution);
|
|
const deltaE = newEnergy - energy;
|
|
|
|
// Metropolis-Hastings with quantum tunneling
|
|
if (deltaE < 0 || Math.random() < Math.exp(-deltaE / temp) + tunnelProb * 0.01) {
|
|
solution = newSolution;
|
|
energy = newEnergy;
|
|
|
|
if (energy < this.bestEnergy) {
|
|
this.bestSolution = [...solution];
|
|
this.bestEnergy = energy;
|
|
}
|
|
}
|
|
}
|
|
|
|
temp *= this.config.annealing.coolingRate;
|
|
|
|
if (sweep % 100 === 0) {
|
|
this.history.push({
|
|
sweep,
|
|
temperature: temp,
|
|
energy: energy,
|
|
bestEnergy: this.bestEnergy
|
|
});
|
|
}
|
|
}
|
|
|
|
// Convert binary to weights
|
|
const weights = this.bestSolution.map(b => b / this.bestSolution.reduce((a, b) => a + b, 1));
|
|
|
|
return {
|
|
bestSolution: this.bestSolution,
|
|
bestWeights: weights,
|
|
bestEnergy: this.bestEnergy,
|
|
history: this.history
|
|
};
|
|
}
|
|
}
|
|
|
|
// Classical portfolio optimizer for comparison
|
|
class ClassicalOptimizer {
|
|
optimize(expectedReturns, covarianceMatrix, riskAversion) {
|
|
const n = expectedReturns.length;
|
|
|
|
// Simple gradient descent on Markowitz objective
|
|
let weights = new Array(n).fill(1 / n);
|
|
const lr = 0.01;
|
|
|
|
for (let iter = 0; iter < 1000; iter++) {
|
|
// Calculate gradient
|
|
const gradients = new Array(n).fill(0);
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
// d/dw_i of (-return + riskAversion * variance)
|
|
gradients[i] = -expectedReturns[i];
|
|
|
|
for (let j = 0; j < n; j++) {
|
|
gradients[i] += 2 * riskAversion * covarianceMatrix[i][j] * weights[j];
|
|
}
|
|
}
|
|
|
|
// Update weights
|
|
for (let i = 0; i < n; i++) {
|
|
weights[i] -= lr * gradients[i];
|
|
weights[i] = Math.max(0, weights[i]); // Non-negative
|
|
}
|
|
|
|
// Normalize
|
|
const total = weights.reduce((a, b) => a + b, 0);
|
|
weights = weights.map(w => w / total);
|
|
}
|
|
|
|
// Calculate final metrics
|
|
const expectedReturn = weights.reduce((sum, w, i) => sum + w * expectedReturns[i], 0);
|
|
let variance = 0;
|
|
for (let i = 0; i < n; i++) {
|
|
for (let j = 0; j < n; j++) {
|
|
variance += weights[i] * weights[j] * covarianceMatrix[i][j];
|
|
}
|
|
}
|
|
|
|
return {
|
|
weights,
|
|
expectedReturn,
|
|
variance,
|
|
sharpe: expectedReturn / Math.sqrt(variance)
|
|
};
|
|
}
|
|
}
|
|
|
|
// Generate synthetic market data
|
|
function generateMarketData(numAssets, seed = 42) {
|
|
let rng = seed;
|
|
const random = () => {
|
|
rng = (rng * 9301 + 49297) % 233280;
|
|
return rng / 233280;
|
|
};
|
|
|
|
const assetNames = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'NVDA', 'JPM', 'BAC', 'XOM', 'JNJ', 'WMT'];
|
|
|
|
// Generate expected returns (5-15% annualized)
|
|
const expectedReturns = [];
|
|
for (let i = 0; i < numAssets; i++) {
|
|
expectedReturns.push(0.05 + random() * 0.10);
|
|
}
|
|
|
|
// Generate covariance matrix (positive semi-definite)
|
|
const volatilities = [];
|
|
for (let i = 0; i < numAssets; i++) {
|
|
volatilities.push(0.15 + random() * 0.20); // 15-35% vol
|
|
}
|
|
|
|
// Correlation matrix with sector structure
|
|
const correlations = Array(numAssets).fill(null).map(() => Array(numAssets).fill(0));
|
|
for (let i = 0; i < numAssets; i++) {
|
|
for (let j = 0; j < numAssets; j++) {
|
|
if (i === j) {
|
|
correlations[i][j] = 1;
|
|
} else {
|
|
// Higher correlation within "sectors"
|
|
const sameSector = Math.floor(i / 3) === Math.floor(j / 3);
|
|
correlations[i][j] = sameSector ? 0.5 + random() * 0.3 : 0.2 + random() * 0.3;
|
|
correlations[j][i] = correlations[i][j];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Covariance = correlation * vol_i * vol_j
|
|
const covarianceMatrix = Array(numAssets).fill(null).map(() => Array(numAssets).fill(0));
|
|
for (let i = 0; i < numAssets; i++) {
|
|
for (let j = 0; j < numAssets; j++) {
|
|
covarianceMatrix[i][j] = correlations[i][j] * volatilities[i] * volatilities[j];
|
|
}
|
|
}
|
|
|
|
return {
|
|
assetNames: assetNames.slice(0, numAssets),
|
|
expectedReturns,
|
|
volatilities,
|
|
correlations,
|
|
covarianceMatrix
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
console.log('═'.repeat(70));
|
|
console.log('QUANTUM-INSPIRED PORTFOLIO OPTIMIZATION');
|
|
console.log('═'.repeat(70));
|
|
console.log();
|
|
|
|
// 1. Generate market data
|
|
console.log('1. Market Data Generation:');
|
|
console.log('─'.repeat(70));
|
|
|
|
const numAssets = quantumConfig.portfolio.numAssets;
|
|
const marketData = generateMarketData(numAssets);
|
|
|
|
console.log(` Assets: ${numAssets}`);
|
|
console.log(` Risk aversion: ${quantumConfig.portfolio.riskAversion}`);
|
|
console.log(` Max weight: ${(quantumConfig.portfolio.maxWeight * 100).toFixed(0)}%`);
|
|
console.log(` Cardinality: Max ${quantumConfig.portfolio.cardinalityConstraint} assets`);
|
|
console.log();
|
|
|
|
console.log(' Asset Characteristics:');
|
|
console.log(' Asset │ E[R] │ Vol │');
|
|
console.log('─'.repeat(70));
|
|
for (let i = 0; i < Math.min(5, numAssets); i++) {
|
|
console.log(` ${marketData.assetNames[i].padEnd(5)} │ ${(marketData.expectedReturns[i] * 100).toFixed(1)}% │ ${(marketData.volatilities[i] * 100).toFixed(1)}% │`);
|
|
}
|
|
console.log(` ... (${numAssets - 5} more assets)`);
|
|
console.log();
|
|
|
|
// 2. Classical optimization (baseline)
|
|
console.log('2. Classical Optimization (Baseline):');
|
|
console.log('─'.repeat(70));
|
|
|
|
const classical = new ClassicalOptimizer();
|
|
const classicalResult = classical.optimize(
|
|
marketData.expectedReturns,
|
|
marketData.covarianceMatrix,
|
|
quantumConfig.portfolio.riskAversion
|
|
);
|
|
|
|
console.log(` Expected Return: ${(classicalResult.expectedReturn * 100).toFixed(2)}%`);
|
|
console.log(` Portfolio Vol: ${(Math.sqrt(classicalResult.variance) * 100).toFixed(2)}%`);
|
|
console.log(` Sharpe Ratio: ${classicalResult.sharpe.toFixed(3)}`);
|
|
console.log();
|
|
|
|
console.log(' Weights:');
|
|
const sortedClassical = classicalResult.weights
|
|
.map((w, i) => ({ name: marketData.assetNames[i], weight: w }))
|
|
.sort((a, b) => b.weight - a.weight)
|
|
.slice(0, 5);
|
|
|
|
for (const { name, weight } of sortedClassical) {
|
|
const bar = '█'.repeat(Math.floor(weight * 40));
|
|
console.log(` ${name.padEnd(5)} ${bar.padEnd(40)} ${(weight * 100).toFixed(1)}%`);
|
|
}
|
|
console.log();
|
|
|
|
// 3. Quantum Annealing
|
|
console.log('3. Quantum Annealing Optimization:');
|
|
console.log('─'.repeat(70));
|
|
|
|
const annealer = new QuantumAnnealer(quantumConfig);
|
|
const annealingResult = annealer.runAnnealing(
|
|
marketData.expectedReturns,
|
|
marketData.covarianceMatrix
|
|
);
|
|
|
|
// Calculate metrics for annealing result
|
|
const annealingReturn = annealingResult.bestWeights.reduce(
|
|
(sum, w, i) => sum + w * marketData.expectedReturns[i], 0
|
|
);
|
|
let annealingVariance = 0;
|
|
for (let i = 0; i < numAssets; i++) {
|
|
for (let j = 0; j < numAssets; j++) {
|
|
annealingVariance += annealingResult.bestWeights[i] * annealingResult.bestWeights[j] *
|
|
marketData.covarianceMatrix[i][j];
|
|
}
|
|
}
|
|
|
|
console.log(` Expected Return: ${(annealingReturn * 100).toFixed(2)}%`);
|
|
console.log(` Portfolio Vol: ${(Math.sqrt(annealingVariance) * 100).toFixed(2)}%`);
|
|
console.log(` Sharpe Ratio: ${(annealingReturn / Math.sqrt(annealingVariance)).toFixed(3)}`);
|
|
console.log(` Final Energy: ${annealingResult.bestEnergy.toFixed(4)}`);
|
|
console.log();
|
|
|
|
console.log(' Binary Solution:');
|
|
console.log(` ${annealingResult.bestSolution.join('')}`);
|
|
console.log();
|
|
|
|
console.log(' Weights:');
|
|
const sortedAnnealing = annealingResult.bestWeights
|
|
.map((w, i) => ({ name: marketData.assetNames[i], weight: w }))
|
|
.sort((a, b) => b.weight - a.weight)
|
|
.filter(x => x.weight > 0.01)
|
|
.slice(0, 5);
|
|
|
|
for (const { name, weight } of sortedAnnealing) {
|
|
const bar = '█'.repeat(Math.floor(weight * 40));
|
|
console.log(` ${name.padEnd(5)} ${bar.padEnd(40)} ${(weight * 100).toFixed(1)}%`);
|
|
}
|
|
console.log();
|
|
|
|
// 4. QAOA (simplified)
|
|
console.log('4. QAOA Optimization (Simplified):');
|
|
console.log('─'.repeat(70));
|
|
|
|
// Use smaller problem for QAOA simulation
|
|
const qaoaConfig = { ...quantumConfig, portfolio: { ...quantumConfig.portfolio, numAssets: 4 } };
|
|
const smallMarketData = generateMarketData(4);
|
|
|
|
const qaoa = new QAOAOptimizer(qaoaConfig);
|
|
const qaoaResult = qaoa.runQAOA(
|
|
smallMarketData.expectedReturns,
|
|
smallMarketData.covarianceMatrix
|
|
);
|
|
|
|
console.log(` QAOA Layers (p): ${quantumConfig.qaoa.layers}`);
|
|
console.log(` Measurement shots: ${quantumConfig.qaoa.shots}`);
|
|
console.log(` Best Cost: ${qaoaResult.bestCost.toFixed(4)}`);
|
|
console.log();
|
|
|
|
console.log(' QAOA Weights (4-asset subset):');
|
|
for (let i = 0; i < qaoaResult.bestWeights.length; i++) {
|
|
const w = qaoaResult.bestWeights[i];
|
|
const bar = '█'.repeat(Math.floor(w * 40));
|
|
console.log(` Asset ${i + 1} ${bar.padEnd(40)} ${(w * 100).toFixed(1)}%`);
|
|
}
|
|
console.log();
|
|
|
|
// 5. Annealing convergence
|
|
console.log('5. Annealing Convergence:');
|
|
console.log('─'.repeat(70));
|
|
|
|
console.log(' Energy vs Temperature:');
|
|
let curve = ' ';
|
|
const energies = annealingResult.history.map(h => h.energy);
|
|
const minE = Math.min(...energies);
|
|
const maxE = Math.max(...energies);
|
|
const rangeE = maxE - minE || 1;
|
|
|
|
for (const h of annealingResult.history.slice(-40)) {
|
|
const norm = 1 - (h.energy - minE) / rangeE;
|
|
if (norm < 0.25) curve += '▁';
|
|
else if (norm < 0.5) curve += '▃';
|
|
else if (norm < 0.75) curve += '▅';
|
|
else curve += '█';
|
|
}
|
|
console.log(curve);
|
|
console.log(` Start Energy: ${maxE.toFixed(3)} Final: ${minE.toFixed(3)}`);
|
|
console.log();
|
|
|
|
// 6. Quantum advantage discussion
|
|
console.log('6. Quantum Advantage Analysis:');
|
|
console.log('─'.repeat(70));
|
|
|
|
console.log(' Problem Complexity:');
|
|
console.log(` - Classical: O(n³) for Markowitz with constraints`);
|
|
console.log(` - Quantum: O(√n) potential speedup via Grover`);
|
|
console.log();
|
|
|
|
console.log(' This simulation demonstrates:');
|
|
console.log(' - QUBO formulation of portfolio optimization');
|
|
console.log(' - Quantum annealing energy landscape exploration');
|
|
console.log(' - QAOA variational quantum-classical hybrid');
|
|
console.log();
|
|
|
|
console.log(' Real quantum hardware benefits:');
|
|
console.log(' - Combinatorial (cardinality) constraints');
|
|
console.log(' - Large-scale problems (1000+ assets)');
|
|
console.log(' - Non-convex objectives');
|
|
console.log();
|
|
|
|
// 7. Comparison summary
|
|
console.log('7. Method Comparison:');
|
|
console.log('─'.repeat(70));
|
|
|
|
const classicalSharpe = classicalResult.sharpe;
|
|
const annealingSharpe = annealingReturn / Math.sqrt(annealingVariance);
|
|
|
|
console.log(' Method │ Return │ Vol │ Sharpe │ Assets');
|
|
console.log('─'.repeat(70));
|
|
console.log(` Classical │ ${(classicalResult.expectedReturn * 100).toFixed(1)}% │ ${(Math.sqrt(classicalResult.variance) * 100).toFixed(1)}% │ ${classicalSharpe.toFixed(3)} │ ${classicalResult.weights.filter(w => w > 0.01).length}`);
|
|
console.log(` Quantum Anneal │ ${(annealingReturn * 100).toFixed(1)}% │ ${(Math.sqrt(annealingVariance) * 100).toFixed(1)}% │ ${annealingSharpe.toFixed(3)} │ ${annealingResult.bestWeights.filter(w => w > 0.01).length}`);
|
|
console.log();
|
|
|
|
// 8. RuVector integration
|
|
console.log('8. RuVector Vector Storage:');
|
|
console.log('─'.repeat(70));
|
|
console.log(' Portfolio weight vectors can be stored:');
|
|
console.log();
|
|
console.log(` Classical weights: [${classicalResult.weights.slice(0, 4).map(w => w.toFixed(3)).join(', ')}, ...]`);
|
|
console.log(` Quantum weights: [${annealingResult.bestWeights.slice(0, 4).map(w => w.toFixed(3)).join(', ')}, ...]`);
|
|
console.log();
|
|
console.log(' Use cases:');
|
|
console.log(' - Similarity search for portfolio allocation patterns');
|
|
console.log(' - Regime-based portfolio retrieval');
|
|
console.log(' - Factor exposure analysis via vector operations');
|
|
console.log();
|
|
|
|
console.log('═'.repeat(70));
|
|
console.log('Quantum-inspired portfolio optimization completed');
|
|
console.log('═'.repeat(70));
|
|
}
|
|
|
|
main().catch(console.error);
|