Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
624
examples/neural-trader/exotic/hyperbolic-embeddings.js
Normal file
624
examples/neural-trader/exotic/hyperbolic-embeddings.js
Normal file
@@ -0,0 +1,624 @@
|
||||
/**
|
||||
* Hyperbolic Market Embeddings
|
||||
*
|
||||
* EXOTIC: Poincaré disk embeddings for hierarchical market structure
|
||||
*
|
||||
* Uses @neural-trader/neural with RuVector for:
|
||||
* - Poincaré ball model for hyperbolic geometry
|
||||
* - Exponential capacity for tree-like hierarchies
|
||||
* - Market taxonomy learning (sector → industry → company)
|
||||
* - Distance preservation in curved space
|
||||
*
|
||||
* Hyperbolic space naturally represents hierarchical relationships
|
||||
* that exist in markets (market → sector → industry → stock).
|
||||
*/
|
||||
|
||||
// Hyperbolic embedding configuration
|
||||
const hyperbolicConfig = {
|
||||
// Embedding parameters
|
||||
embedding: {
|
||||
dimension: 2, // 2D for visualization, can be higher
|
||||
curvature: -1, // Negative curvature (hyperbolic)
|
||||
learningRate: 0.01,
|
||||
epochs: 100,
|
||||
negSamples: 5 // Negative sampling
|
||||
},
|
||||
|
||||
// Market hierarchy
|
||||
hierarchy: {
|
||||
levels: ['Market', 'Sector', 'Industry', 'Stock'],
|
||||
useCorrelations: true // Learn from return correlations
|
||||
},
|
||||
|
||||
// Poincaré ball constraints
|
||||
poincare: {
|
||||
maxNorm: 0.99, // Stay inside unit ball
|
||||
epsilon: 1e-5 // Numerical stability
|
||||
}
|
||||
};
|
||||
|
||||
// Poincaré Ball Operations
|
||||
class PoincareOperations {
|
||||
constructor(curvature = -1) {
|
||||
this.c = Math.abs(curvature);
|
||||
this.sqrtC = Math.sqrt(this.c);
|
||||
}
|
||||
|
||||
// Möbius addition: x ⊕ y
|
||||
mobiusAdd(x, y) {
|
||||
const c = this.c;
|
||||
const xNorm2 = x.reduce((s, v) => s + v * v, 0);
|
||||
const yNorm2 = y.reduce((s, v) => s + v * v, 0);
|
||||
const xy = x.reduce((s, v, i) => s + v * y[i], 0);
|
||||
|
||||
const denom = 1 + 2 * c * xy + c * c * xNorm2 * yNorm2;
|
||||
|
||||
return x.map((xi, i) => {
|
||||
const num = (1 + 2 * c * xy + c * yNorm2) * xi + (1 - c * xNorm2) * y[i];
|
||||
return num / denom;
|
||||
});
|
||||
}
|
||||
|
||||
// Poincaré distance with numerical stability
|
||||
distance(x, y) {
|
||||
const diff = x.map((v, i) => v - y[i]);
|
||||
const diffNorm2 = diff.reduce((s, v) => s + v * v, 0);
|
||||
const xNorm2 = x.reduce((s, v) => s + v * v, 0);
|
||||
const yNorm2 = y.reduce((s, v) => s + v * v, 0);
|
||||
|
||||
// Ensure points are inside the ball
|
||||
const eps = hyperbolicConfig.poincare.epsilon;
|
||||
const safeXNorm2 = Math.min(xNorm2, 1 - eps);
|
||||
const safeYNorm2 = Math.min(yNorm2, 1 - eps);
|
||||
|
||||
const num = 2 * diffNorm2;
|
||||
const denom = (1 - safeXNorm2) * (1 - safeYNorm2);
|
||||
|
||||
// Guard against numerical issues with Math.acosh (arg must be >= 1)
|
||||
const arg = 1 + num / Math.max(denom, eps);
|
||||
const safeArg = Math.max(1, arg); // acosh domain is [1, inf)
|
||||
|
||||
return Math.acosh(safeArg) / this.sqrtC;
|
||||
}
|
||||
|
||||
// Exponential map: tangent space → manifold
|
||||
expMap(x, v) {
|
||||
const c = this.c;
|
||||
const vNorm = Math.sqrt(v.reduce((s, vi) => s + vi * vi, 0)) + hyperbolicConfig.poincare.epsilon;
|
||||
const xNorm2 = x.reduce((s, xi) => s + xi * xi, 0);
|
||||
|
||||
const lambda = 2 / (1 - c * xNorm2);
|
||||
const t = Math.tanh(this.sqrtC * lambda * vNorm / 2);
|
||||
|
||||
const y = v.map(vi => t * vi / (this.sqrtC * vNorm));
|
||||
|
||||
return this.mobiusAdd(x, y);
|
||||
}
|
||||
|
||||
// Logarithmic map: manifold → tangent space
|
||||
logMap(x, y) {
|
||||
const c = this.c;
|
||||
const eps = hyperbolicConfig.poincare.epsilon;
|
||||
const xNorm2 = Math.min(x.reduce((s, xi) => s + xi * xi, 0), 1 - eps);
|
||||
const lambda = 2 / Math.max(1 - c * xNorm2, eps);
|
||||
|
||||
const mxy = this.mobiusAdd(x.map(v => -v), y);
|
||||
const mxyNorm = Math.sqrt(mxy.reduce((s, v) => s + v * v, 0)) + eps;
|
||||
|
||||
// Guard atanh domain: argument must be in (-1, 1)
|
||||
const atanhArg = Math.min(this.sqrtC * mxyNorm, 1 - eps);
|
||||
const t = Math.atanh(atanhArg);
|
||||
|
||||
return mxy.map(v => 2 * t * v / (lambda * this.sqrtC * mxyNorm));
|
||||
}
|
||||
|
||||
// Project to Poincaré ball (ensure ||x|| < 1)
|
||||
project(x) {
|
||||
const norm = Math.sqrt(x.reduce((s, v) => s + v * v, 0));
|
||||
const maxNorm = hyperbolicConfig.poincare.maxNorm;
|
||||
|
||||
if (norm >= maxNorm) {
|
||||
return x.map(v => v * maxNorm / norm);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
// Riemannian gradient (for optimization)
|
||||
riemannianGrad(x, euclideanGrad) {
|
||||
const xNorm2 = x.reduce((s, v) => s + v * v, 0);
|
||||
const scale = Math.pow((1 - this.c * xNorm2), 2) / 4;
|
||||
|
||||
return euclideanGrad.map(g => g * scale);
|
||||
}
|
||||
}
|
||||
|
||||
// Hyperbolic Embedding Model
|
||||
class HyperbolicEmbedding {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.poincare = new PoincareOperations(config.embedding.curvature);
|
||||
this.embeddings = new Map();
|
||||
this.hierarchyGraph = new Map();
|
||||
this.losses = [];
|
||||
}
|
||||
|
||||
// Initialize embedding for entity
|
||||
initEmbedding(entity) {
|
||||
const dim = this.config.embedding.dimension;
|
||||
|
||||
// Initialize near origin (parent entities will move toward center)
|
||||
const embedding = [];
|
||||
for (let i = 0; i < dim; i++) {
|
||||
embedding.push((Math.random() - 0.5) * 0.1);
|
||||
}
|
||||
|
||||
this.embeddings.set(entity, this.poincare.project(embedding));
|
||||
}
|
||||
|
||||
// Add hierarchy relationship
|
||||
addHierarchy(parent, child) {
|
||||
if (!this.hierarchyGraph.has(parent)) {
|
||||
this.hierarchyGraph.set(parent, { children: [], parent: null });
|
||||
}
|
||||
if (!this.hierarchyGraph.has(child)) {
|
||||
this.hierarchyGraph.set(child, { children: [], parent: null });
|
||||
}
|
||||
|
||||
this.hierarchyGraph.get(parent).children.push(child);
|
||||
this.hierarchyGraph.get(child).parent = parent;
|
||||
|
||||
// Initialize embeddings
|
||||
if (!this.embeddings.has(parent)) this.initEmbedding(parent);
|
||||
if (!this.embeddings.has(child)) this.initEmbedding(child);
|
||||
}
|
||||
|
||||
// Training loss: children should be farther from origin than parents
|
||||
computeLoss(parent, child) {
|
||||
const pEmb = this.embeddings.get(parent);
|
||||
const cEmb = this.embeddings.get(child);
|
||||
|
||||
if (!pEmb || !cEmb) return 0;
|
||||
|
||||
// Distance from origin
|
||||
const pDist = Math.sqrt(pEmb.reduce((s, v) => s + v * v, 0));
|
||||
const cDist = Math.sqrt(cEmb.reduce((s, v) => s + v * v, 0));
|
||||
|
||||
// Parent should be closer to origin
|
||||
const hierarchyLoss = Math.max(0, pDist - cDist + 0.1);
|
||||
|
||||
// Parent-child should be close
|
||||
const distLoss = this.poincare.distance(pEmb, cEmb);
|
||||
|
||||
return hierarchyLoss + 0.5 * distLoss;
|
||||
}
|
||||
|
||||
// Train embeddings
|
||||
train() {
|
||||
const lr = this.config.embedding.learningRate;
|
||||
|
||||
for (let epoch = 0; epoch < this.config.embedding.epochs; epoch++) {
|
||||
let totalLoss = 0;
|
||||
|
||||
// For each parent-child pair
|
||||
for (const [entity, info] of this.hierarchyGraph) {
|
||||
for (const child of info.children) {
|
||||
const loss = this.computeLoss(entity, child);
|
||||
totalLoss += loss;
|
||||
|
||||
// Gradient update (simplified)
|
||||
this.updateEmbedding(entity, child, lr);
|
||||
}
|
||||
}
|
||||
|
||||
this.losses.push(totalLoss);
|
||||
|
||||
// Decay learning rate
|
||||
if (epoch % 20 === 0) {
|
||||
// lr *= 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Riemannian gradient descent update
|
||||
updateEmbedding(parent, child, lr) {
|
||||
const pEmb = this.embeddings.get(parent);
|
||||
const cEmb = this.embeddings.get(child);
|
||||
const eps = hyperbolicConfig.poincare.epsilon;
|
||||
|
||||
// Compute Euclidean gradients
|
||||
const pNorm2 = pEmb.reduce((s, v) => s + v * v, 0);
|
||||
const cNorm2 = cEmb.reduce((s, v) => s + v * v, 0);
|
||||
|
||||
// Gradient for parent: move toward origin (hierarchy constraint)
|
||||
const pGradEuclid = pEmb.map(v => v); // gradient of ||x||^2 is 2x
|
||||
|
||||
// Gradient for child: move toward parent but stay farther from origin
|
||||
const direction = cEmb.map((v, i) => pEmb[i] - v);
|
||||
const dirNorm = Math.sqrt(direction.reduce((s, d) => s + d * d, 0)) + eps;
|
||||
const normalizedDir = direction.map(d => d / dirNorm);
|
||||
|
||||
// Child gradient: toward parent + outward from origin
|
||||
const cGradEuclid = cEmb.map((v, i) => -normalizedDir[i] * 0.3 - v * 0.1);
|
||||
|
||||
// Convert to Riemannian gradients using metric tensor
|
||||
const pRiemannGrad = this.poincare.riemannianGrad(pEmb, pGradEuclid);
|
||||
const cRiemannGrad = this.poincare.riemannianGrad(cEmb, cGradEuclid);
|
||||
|
||||
// Update using exponential map (proper Riemannian SGD)
|
||||
const pTangent = pRiemannGrad.map(g => -lr * g);
|
||||
const cTangent = cRiemannGrad.map(g => -lr * g);
|
||||
|
||||
const newPEmb = this.poincare.expMap(pEmb, pTangent);
|
||||
const newCEmb = this.poincare.expMap(cEmb, cTangent);
|
||||
|
||||
this.embeddings.set(parent, this.poincare.project(newPEmb));
|
||||
this.embeddings.set(child, this.poincare.project(newCEmb));
|
||||
}
|
||||
|
||||
// Get embedding
|
||||
getEmbedding(entity) {
|
||||
return this.embeddings.get(entity);
|
||||
}
|
||||
|
||||
// Find nearest neighbors in hyperbolic space
|
||||
findNearest(entity, k = 5) {
|
||||
const emb = this.embeddings.get(entity);
|
||||
if (!emb) return [];
|
||||
|
||||
const distances = [];
|
||||
for (const [other, otherEmb] of this.embeddings) {
|
||||
if (other !== entity) {
|
||||
distances.push({
|
||||
entity: other,
|
||||
distance: this.poincare.distance(emb, otherEmb)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return distances.sort((a, b) => a.distance - b.distance).slice(0, k);
|
||||
}
|
||||
|
||||
// Get depth (distance from origin)
|
||||
getDepth(entity) {
|
||||
const emb = this.embeddings.get(entity);
|
||||
if (!emb) return 0;
|
||||
return Math.sqrt(emb.reduce((s, v) => s + v * v, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Market hierarchy builder
|
||||
class MarketHierarchy {
|
||||
constructor() {
|
||||
this.sectors = {
|
||||
'Technology': ['Software', 'Hardware', 'Semiconductors'],
|
||||
'Healthcare': ['Pharma', 'Biotech', 'MedDevices'],
|
||||
'Finance': ['Banks', 'Insurance', 'AssetMgmt'],
|
||||
'Energy': ['Oil', 'Gas', 'Renewables'],
|
||||
'Consumer': ['Retail', 'FoodBev', 'Apparel']
|
||||
};
|
||||
|
||||
this.industries = {
|
||||
'Software': ['MSFT', 'ORCL', 'CRM'],
|
||||
'Hardware': ['AAPL', 'DELL', 'HPQ'],
|
||||
'Semiconductors': ['NVDA', 'AMD', 'INTC'],
|
||||
'Pharma': ['JNJ', 'PFE', 'MRK'],
|
||||
'Biotech': ['AMGN', 'GILD', 'BIIB'],
|
||||
'MedDevices': ['MDT', 'ABT', 'SYK'],
|
||||
'Banks': ['JPM', 'BAC', 'WFC'],
|
||||
'Insurance': ['BRK', 'MET', 'AIG'],
|
||||
'AssetMgmt': ['BLK', 'GS', 'MS'],
|
||||
'Oil': ['XOM', 'CVX', 'COP'],
|
||||
'Gas': ['SLB', 'HAL', 'BKR'],
|
||||
'Renewables': ['NEE', 'ENPH', 'SEDG'],
|
||||
'Retail': ['AMZN', 'WMT', 'TGT'],
|
||||
'FoodBev': ['KO', 'PEP', 'MCD'],
|
||||
'Apparel': ['NKE', 'LULU', 'TJX']
|
||||
};
|
||||
}
|
||||
|
||||
buildHierarchy(embedding) {
|
||||
// Market → Sectors
|
||||
for (const sector of Object.keys(this.sectors)) {
|
||||
embedding.addHierarchy('Market', sector);
|
||||
|
||||
// Sector → Industries
|
||||
for (const industry of this.sectors[sector]) {
|
||||
embedding.addHierarchy(sector, industry);
|
||||
|
||||
// Industry → Stocks
|
||||
if (this.industries[industry]) {
|
||||
for (const stock of this.industries[industry]) {
|
||||
embedding.addHierarchy(industry, stock);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAllStocks() {
|
||||
const stocks = [];
|
||||
for (const industry of Object.values(this.industries)) {
|
||||
stocks.push(...industry);
|
||||
}
|
||||
return stocks;
|
||||
}
|
||||
}
|
||||
|
||||
// Visualization helper
|
||||
class HyperbolicVisualizer {
|
||||
visualize(embedding, width = 40, height = 20) {
|
||||
const grid = [];
|
||||
for (let i = 0; i < height; i++) {
|
||||
grid.push(new Array(width).fill(' '));
|
||||
}
|
||||
|
||||
// Draw unit circle boundary
|
||||
for (let angle = 0; angle < 2 * Math.PI; angle += 0.1) {
|
||||
const x = Math.cos(angle) * 0.95;
|
||||
const y = Math.sin(angle) * 0.95;
|
||||
|
||||
const gridX = Math.floor((x + 1) / 2 * (width - 1));
|
||||
const gridY = Math.floor((1 - y) / 2 * (height - 1));
|
||||
|
||||
if (gridY >= 0 && gridY < height && gridX >= 0 && gridX < width) {
|
||||
grid[gridY][gridX] = '·';
|
||||
}
|
||||
}
|
||||
|
||||
// Plot embeddings
|
||||
const symbols = {
|
||||
market: '◉',
|
||||
sector: '●',
|
||||
industry: '○',
|
||||
stock: '·'
|
||||
};
|
||||
|
||||
for (const [entity, emb] of embedding.embeddings) {
|
||||
const x = emb[0];
|
||||
const y = emb[1];
|
||||
|
||||
const gridX = Math.floor((x + 1) / 2 * (width - 1));
|
||||
const gridY = Math.floor((1 - y) / 2 * (height - 1));
|
||||
|
||||
if (gridY >= 0 && gridY < height && gridX >= 0 && gridX < width) {
|
||||
let symbol = '?';
|
||||
if (entity === 'Market') symbol = symbols.market;
|
||||
else if (['Technology', 'Healthcare', 'Finance', 'Energy', 'Consumer'].includes(entity)) symbol = symbols.sector;
|
||||
else if (entity.length > 4) symbol = symbols.industry;
|
||||
else symbol = symbols.stock;
|
||||
|
||||
grid[gridY][gridX] = symbol;
|
||||
}
|
||||
}
|
||||
|
||||
return grid.map(row => row.join('')).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('═'.repeat(70));
|
||||
console.log('HYPERBOLIC MARKET EMBEDDINGS');
|
||||
console.log('═'.repeat(70));
|
||||
console.log();
|
||||
|
||||
// 1. Build market hierarchy
|
||||
console.log('1. Market Hierarchy Construction:');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const hierarchy = new MarketHierarchy();
|
||||
const embedding = new HyperbolicEmbedding(hyperbolicConfig);
|
||||
|
||||
hierarchy.buildHierarchy(embedding);
|
||||
|
||||
console.log(` Levels: ${hyperbolicConfig.hierarchy.levels.join(' → ')}`);
|
||||
console.log(` Sectors: ${Object.keys(hierarchy.sectors).length}`);
|
||||
console.log(` Industries: ${Object.keys(hierarchy.industries).length}`);
|
||||
console.log(` Stocks: ${hierarchy.getAllStocks().length}`);
|
||||
console.log(` Dimension: ${hyperbolicConfig.embedding.dimension}D Poincaré ball`);
|
||||
console.log();
|
||||
|
||||
// 2. Train embeddings
|
||||
console.log('2. Training Hyperbolic Embeddings:');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
embedding.train();
|
||||
|
||||
console.log(` Epochs: ${hyperbolicConfig.embedding.epochs}`);
|
||||
console.log(` Learning rate: ${hyperbolicConfig.embedding.learningRate}`);
|
||||
console.log(` Initial loss: ${embedding.losses[0]?.toFixed(4) || 'N/A'}`);
|
||||
console.log(` Final loss: ${embedding.losses[embedding.losses.length - 1]?.toFixed(4) || 'N/A'}`);
|
||||
console.log();
|
||||
|
||||
// 3. Embedding depths
|
||||
console.log('3. Hierarchy Depth Analysis:');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
console.log(' Entity depths (distance from origin):');
|
||||
console.log();
|
||||
|
||||
// Market (root)
|
||||
const marketDepth = embedding.getDepth('Market');
|
||||
console.log(` Market (root): ${marketDepth.toFixed(4)}`);
|
||||
|
||||
// Sectors
|
||||
let avgSectorDepth = 0;
|
||||
for (const sector of Object.keys(hierarchy.sectors)) {
|
||||
avgSectorDepth += embedding.getDepth(sector);
|
||||
}
|
||||
avgSectorDepth /= Object.keys(hierarchy.sectors).length;
|
||||
console.log(` Sectors (avg): ${avgSectorDepth.toFixed(4)}`);
|
||||
|
||||
// Industries
|
||||
let avgIndustryDepth = 0;
|
||||
let industryCount = 0;
|
||||
for (const industry of Object.keys(hierarchy.industries)) {
|
||||
avgIndustryDepth += embedding.getDepth(industry);
|
||||
industryCount++;
|
||||
}
|
||||
avgIndustryDepth /= industryCount;
|
||||
console.log(` Industries (avg): ${avgIndustryDepth.toFixed(4)}`);
|
||||
|
||||
// Stocks
|
||||
let avgStockDepth = 0;
|
||||
const stocks = hierarchy.getAllStocks();
|
||||
for (const stock of stocks) {
|
||||
avgStockDepth += embedding.getDepth(stock);
|
||||
}
|
||||
avgStockDepth /= stocks.length;
|
||||
console.log(` Stocks (avg): ${avgStockDepth.toFixed(4)}`);
|
||||
console.log();
|
||||
|
||||
console.log(' Depth increases with hierarchy level ✓');
|
||||
console.log(' (Root near origin, leaves near boundary)');
|
||||
console.log();
|
||||
|
||||
// 4. Sample embeddings
|
||||
console.log('4. Sample Embeddings (2D Poincaré Coordinates):');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const samples = ['Market', 'Technology', 'Software', 'MSFT', 'Finance', 'Banks', 'JPM'];
|
||||
|
||||
console.log(' Entity │ x │ y │ Depth');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
for (const entity of samples) {
|
||||
const emb = embedding.getEmbedding(entity);
|
||||
if (emb) {
|
||||
const depth = embedding.getDepth(entity);
|
||||
console.log(` ${entity.padEnd(16)} │ ${emb[0].toFixed(5).padStart(8)} │ ${emb[1].toFixed(5).padStart(8)} │ ${depth.toFixed(4)}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
|
||||
// 5. Nearest neighbors
|
||||
console.log('5. Nearest Neighbors (Hyperbolic Distance):');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const queryStocks = ['AAPL', 'JPM', 'XOM'];
|
||||
|
||||
for (const stock of queryStocks) {
|
||||
const neighbors = embedding.findNearest(stock, 5);
|
||||
console.log(` ${stock} neighbors:`);
|
||||
for (const { entity, distance } of neighbors) {
|
||||
console.log(` ${entity.padEnd(12)} d=${distance.toFixed(4)}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// 6. Hyperbolic distance properties
|
||||
console.log('6. Hyperbolic Distance Properties:');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const poincare = embedding.poincare;
|
||||
|
||||
// Same industry
|
||||
const samIndustry = poincare.distance(
|
||||
embedding.getEmbedding('MSFT'),
|
||||
embedding.getEmbedding('ORCL')
|
||||
);
|
||||
|
||||
// Same sector, different industry
|
||||
const sameSector = poincare.distance(
|
||||
embedding.getEmbedding('MSFT'),
|
||||
embedding.getEmbedding('NVDA')
|
||||
);
|
||||
|
||||
// Different sector
|
||||
const diffSector = poincare.distance(
|
||||
embedding.getEmbedding('MSFT'),
|
||||
embedding.getEmbedding('JPM')
|
||||
);
|
||||
|
||||
console.log(' Distance comparisons:');
|
||||
console.log(` MSFT ↔ ORCL (same industry): ${samIndustry.toFixed(4)}`);
|
||||
console.log(` MSFT ↔ NVDA (same sector): ${sameSector.toFixed(4)}`);
|
||||
console.log(` MSFT ↔ JPM (diff sector): ${diffSector.toFixed(4)}`);
|
||||
console.log();
|
||||
console.log(' Distances increase with hierarchical distance ✓');
|
||||
console.log();
|
||||
|
||||
// 7. Visualization
|
||||
console.log('7. Poincaré Disk Visualization:');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const visualizer = new HyperbolicVisualizer();
|
||||
const viz = visualizer.visualize(embedding);
|
||||
|
||||
console.log(viz);
|
||||
console.log();
|
||||
console.log(' Legend: ◉=Market ●=Sector ○=Industry ·=Stock');
|
||||
console.log();
|
||||
|
||||
// 8. Sector clusters
|
||||
console.log('8. Sector Clustering Analysis:');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
for (const [sector, industries] of Object.entries(hierarchy.sectors).slice(0, 3)) {
|
||||
const sectorEmb = embedding.getEmbedding(sector);
|
||||
|
||||
// Calculate average distance from sector to its stocks
|
||||
let avgDist = 0;
|
||||
let count = 0;
|
||||
|
||||
for (const industry of industries) {
|
||||
const stocks = hierarchy.industries[industry] || [];
|
||||
for (const stock of stocks) {
|
||||
const stockEmb = embedding.getEmbedding(stock);
|
||||
if (stockEmb) {
|
||||
avgDist += poincare.distance(sectorEmb, stockEmb);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
avgDist /= count || 1;
|
||||
|
||||
console.log(` ${sector}:`);
|
||||
console.log(` Avg distance to stocks: ${avgDist.toFixed(4)}`);
|
||||
console.log(` Stocks: ${industries.flatMap(i => hierarchy.industries[i] || []).slice(0, 5).join(', ')}...`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// 9. Trading implications
|
||||
console.log('9. Trading Implications:');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
console.log(' Hyperbolic embeddings enable:');
|
||||
console.log();
|
||||
console.log(' 1. Hierarchical diversification:');
|
||||
console.log(' - Select stocks from different "branches"');
|
||||
console.log(' - Maximize hyperbolic distance for diversification');
|
||||
console.log();
|
||||
console.log(' 2. Sector rotation strategies:');
|
||||
console.log(' - Identify sector centroids');
|
||||
console.log(' - Track rotation by watching centroid distances');
|
||||
console.log();
|
||||
console.log(' 3. Pair trading:');
|
||||
console.log(' - Find pairs with small hyperbolic distance');
|
||||
console.log(' - These stocks should move together');
|
||||
console.log();
|
||||
|
||||
// 10. RuVector integration
|
||||
console.log('10. RuVector Vector Storage:');
|
||||
console.log('─'.repeat(70));
|
||||
console.log(' Hyperbolic embeddings stored as vectors:');
|
||||
console.log();
|
||||
|
||||
const appleEmb = embedding.getEmbedding('AAPL');
|
||||
console.log(` AAPL embedding: [${appleEmb.map(v => v.toFixed(4)).join(', ')}]`);
|
||||
console.log();
|
||||
console.log(' Note: Euclidean HNSW can be used after mapping');
|
||||
console.log(' to tangent space at origin for approximate NN.');
|
||||
console.log();
|
||||
console.log(' Use cases:');
|
||||
console.log(' - Find hierarchically similar stocks');
|
||||
console.log(' - Sector membership inference');
|
||||
console.log(' - Anomaly detection (stocks far from expected position)');
|
||||
console.log();
|
||||
|
||||
console.log('═'.repeat(70));
|
||||
console.log('Hyperbolic market embeddings completed');
|
||||
console.log('═'.repeat(70));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user