From 3a2e8557ceb597f973de970db1c5dc7645cdf1af Mon Sep 17 00:00:00 2001 From: rUv Date: Sat, 13 Dec 2025 19:09:37 +0000 Subject: [PATCH] fix(node): Add metadata support to Rust bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add metadata field to JsVectorEntry (as JSON string) - Add metadata and vector fields to JsSearchResult - Add filter field to JsSearchQuery for filtered searches - Update get() to return metadata - Add VectorDBWrapper in ruvector for automatic JSON conversion - Bump versions: @ruvector/core@0.1.28, ruvector@0.1.35 Fixes #71 šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/ruvector-node/src/lib.rs | 48 +- .../apify/neural-trader-system/src/main.js | 773 ++++++++++++++++++ npm/core/platforms/linux-x64-gnu/package.json | 2 +- npm/packages/core/package.json | 6 +- npm/packages/ruvector/package.json | 2 +- npm/packages/ruvector/src/index.ts | 110 ++- 6 files changed, 928 insertions(+), 13 deletions(-) create mode 100644 examples/apify/neural-trader-system/src/main.js diff --git a/crates/ruvector-node/src/lib.rs b/crates/ruvector-node/src/lib.rs index fcfeb2c6..85b1b16d 100644 --- a/crates/ruvector-node/src/lib.rs +++ b/crates/ruvector-node/src/lib.rs @@ -139,14 +139,21 @@ pub struct JsVectorEntry { pub id: Option, /// Vector data as Float32Array or array of numbers pub vector: Float32Array, + /// Optional metadata as JSON string (use JSON.stringify on objects) + pub metadata: Option, } impl JsVectorEntry { fn to_core(&self) -> Result { + // Parse JSON string to HashMap + let metadata = self.metadata.as_ref().and_then(|s| { + serde_json::from_str::>(s).ok() + }); + Ok(VectorEntry { id: self.id.clone(), vector: self.vector.to_vec(), - metadata: None, + metadata, }) } } @@ -160,14 +167,21 @@ pub struct JsSearchQuery { pub k: u32, /// Optional ef_search parameter for HNSW pub ef_search: Option, + /// Optional metadata filter as JSON string (use JSON.stringify on objects) + pub filter: Option, } impl JsSearchQuery { fn to_core(&self) -> Result { + // Parse JSON string to HashMap + let filter = self.filter.as_ref().and_then(|s| { + serde_json::from_str::>(s).ok() + }); + Ok(SearchQuery { vector: self.vector.to_vec(), k: self.k as usize, - filter: None, + filter, ef_search: self.ef_search.map(|v| v as usize), }) } @@ -175,19 +189,33 @@ impl JsSearchQuery { /// Search result with similarity score #[napi(object)] -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct JsSearchResult { /// Vector ID pub id: String, /// Distance/similarity score (lower is better for distance metrics) pub score: f64, + /// Vector data (if requested) + pub vector: Option, + /// Metadata as JSON string (use JSON.parse to convert to object) + pub metadata: Option, } impl From for JsSearchResult { fn from(result: SearchResult) -> Self { + // Convert Vec to Float32Array + let vector = result.vector.map(|v| Float32Array::new(v)); + + // Convert HashMap to JSON string + let metadata = result.metadata.and_then(|m| { + serde_json::to_string(&m).ok() + }); + JsSearchResult { id: result.id, score: f64::from(result.score), + vector, + metadata, } } } @@ -364,9 +392,17 @@ impl VectorDB { .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Get failed: {}", e)))?; - Ok(result.map(|entry| JsVectorEntry { - id: entry.id, - vector: Float32Array::new(entry.vector), + Ok(result.map(|entry| { + // Convert HashMap to JSON string + let metadata = entry.metadata.and_then(|m| { + serde_json::to_string(&m).ok() + }); + + JsVectorEntry { + id: entry.id, + vector: Float32Array::new(entry.vector), + metadata, + } })) } diff --git a/examples/apify/neural-trader-system/src/main.js b/examples/apify/neural-trader-system/src/main.js new file mode 100644 index 00000000..711cc0d4 --- /dev/null +++ b/examples/apify/neural-trader-system/src/main.js @@ -0,0 +1,773 @@ +import { Actor } from 'apify'; + +// Neural Engine - Core neural network implementation +class NeuralEngine { + constructor(config = {}) { + this.layers = config.layers || 3; + this.neurons = config.neurons || [128, 64, 32]; + this.activation = config.activation || 'relu'; + this.dropout = config.dropout || 0.2; + this.learningRate = config.learningRate || 0.001; + this.weights = []; + this.biases = []; + this.initializeWeights(); + } + + initializeWeights() { + for (let i = 0; i < this.neurons.length; i++) { + const inputSize = i === 0 ? 50 : this.neurons[i - 1]; // 50 input features + const outputSize = this.neurons[i]; + + // Xavier initialization + const limit = Math.sqrt(6 / (inputSize + outputSize)); + this.weights[i] = Array(inputSize).fill(0).map(() => + Array(outputSize).fill(0).map(() => (Math.random() * 2 - 1) * limit) + ); + this.biases[i] = Array(outputSize).fill(0); + } + } + + activate(x, func = this.activation) { + switch (func) { + case 'relu': + return Math.max(0, x); + case 'tanh': + return Math.tanh(x); + case 'sigmoid': + return 1 / (1 + Math.exp(-x)); + case 'leaky_relu': + return x > 0 ? x : 0.01 * x; + default: + return x; + } + } + + forward(input) { + let activations = input; + + for (let i = 0; i < this.weights.length; i++) { + const layer = []; + for (let j = 0; j < this.weights[i][0].length; j++) { + let sum = this.biases[i][j]; + for (let k = 0; k < activations.length; k++) { + sum += activations[k] * this.weights[i][k][j]; + } + layer.push(this.activate(sum)); + } + activations = layer; + + // Apply dropout during training + if (Math.random() < this.dropout) { + activations = activations.map(a => a * (1 - this.dropout)); + } + } + + return activations; + } + + train(inputs, targets, epochs = 100) { + for (let epoch = 0; epoch < epochs; epoch++) { + let totalLoss = 0; + + for (let i = 0; i < inputs.length; i++) { + const output = this.forward(inputs[i]); + const target = targets[i]; + + // Calculate loss (MSE) + const loss = output.reduce((sum, o, idx) => + sum + Math.pow(o - target[idx], 2), 0) / output.length; + totalLoss += loss; + + // Backpropagation (simplified) + this.backward(inputs[i], target, output); + } + + if (epoch % 10 === 0) { + console.log(`Epoch ${epoch}, Loss: ${totalLoss / inputs.length}`); + } + } + } + + backward(input, target, output) { + // Simplified gradient descent + const error = output.map((o, i) => target[i] - o); + + // Update weights and biases + for (let i = this.weights.length - 1; i >= 0; i--) { + for (let j = 0; j < this.weights[i].length; j++) { + for (let k = 0; k < this.weights[i][j].length; k++) { + this.weights[i][j][k] += this.learningRate * error[k] * + (i === 0 ? input[j] : this.weights[i - 1][j][k]); + } + } + for (let j = 0; j < this.biases[i].length; j++) { + this.biases[i][j] += this.learningRate * error[j]; + } + } + } +} + +// LSTM Cell for time series prediction +class LSTMCell { + constructor(inputSize, hiddenSize) { + this.inputSize = inputSize; + this.hiddenSize = hiddenSize; + this.initializeGates(); + } + + initializeGates() { + this.Wf = this.randomMatrix(this.inputSize + this.hiddenSize, this.hiddenSize); + this.Wi = this.randomMatrix(this.inputSize + this.hiddenSize, this.hiddenSize); + this.Wc = this.randomMatrix(this.inputSize + this.hiddenSize, this.hiddenSize); + this.Wo = this.randomMatrix(this.inputSize + this.hiddenSize, this.hiddenSize); + } + + randomMatrix(rows, cols) { + return Array(rows).fill(0).map(() => + Array(cols).fill(0).map(() => (Math.random() * 2 - 1) * 0.1) + ); + } + + sigmoid(x) { + return 1 / (1 + Math.exp(-x)); + } + + forward(input, hiddenState, cellState) { + const combined = [...input, ...hiddenState]; + + // Forget gate + const forgetGate = this.matmul(combined, this.Wf).map(this.sigmoid); + + // Input gate + const inputGate = this.matmul(combined, this.Wi).map(this.sigmoid); + + // Cell candidate + const cellCandidate = this.matmul(combined, this.Wc).map(Math.tanh); + + // Output gate + const outputGate = this.matmul(combined, this.Wo).map(this.sigmoid); + + // New cell state + const newCellState = forgetGate.map((f, i) => + f * cellState[i] + inputGate[i] * cellCandidate[i] + ); + + // New hidden state + const newHiddenState = outputGate.map((o, i) => + o * Math.tanh(newCellState[i]) + ); + + return { hiddenState: newHiddenState, cellState: newCellState }; + } + + matmul(vec, matrix) { + return matrix[0].map((_, col) => + vec.reduce((sum, val, row) => sum + val * matrix[row][col], 0) + ); + } +} + +// Signal Generator with confidence scoring +class SignalGenerator { + constructor(config = {}) { + this.confidenceThreshold = config.confidenceThreshold || 70; + this.patterns = config.patterns || ['all']; + } + + generateSignal(predictions, marketData) { + const signal = { + timestamp: new Date().toISOString(), + symbol: marketData.symbol, + price: marketData.price, + signal: 'HOLD', + confidence: 0, + reasons: [], + target: null, + stopLoss: null, + patterns: [] + }; + + // Analyze predictions + const avgPrediction = predictions.reduce((a, b) => a + b, 0) / predictions.length; + const variance = predictions.reduce((sum, p) => sum + Math.pow(p - avgPrediction, 2), 0) / predictions.length; + const stdDev = Math.sqrt(variance); + + // Calculate confidence (lower variance = higher confidence) + signal.confidence = Math.min(100, (1 - stdDev) * 100); + + // Generate signal based on prediction + if (avgPrediction > 0.6 && signal.confidence >= this.confidenceThreshold) { + signal.signal = 'BUY'; + signal.target = marketData.price * (1 + marketData.takeProfit / 100); + signal.stopLoss = marketData.price * (1 - marketData.stopLoss / 100); + signal.reasons.push(`Neural prediction: ${(avgPrediction * 100).toFixed(2)}%`); + } else if (avgPrediction < 0.4 && signal.confidence >= this.confidenceThreshold) { + signal.signal = 'SELL'; + signal.target = marketData.price * (1 - marketData.takeProfit / 100); + signal.stopLoss = marketData.price * (1 + marketData.stopLoss / 100); + signal.reasons.push(`Neural prediction: ${(avgPrediction * 100).toFixed(2)}%`); + } + + // Pattern recognition + signal.patterns = this.detectPatterns(marketData); + if (signal.patterns.length > 0) { + signal.reasons.push(`Patterns: ${signal.patterns.join(', ')}`); + signal.confidence = Math.min(100, signal.confidence + signal.patterns.length * 5); + } + + return signal; + } + + detectPatterns(marketData) { + const patterns = []; + const { prices } = marketData; + + if (!prices || prices.length < 5) return patterns; + + // Head and Shoulders + if (this.patterns.includes('all') || this.patterns.includes('head_shoulders')) { + if (this.isHeadAndShoulders(prices)) { + patterns.push('head_shoulders'); + } + } + + // Double Top + if (this.patterns.includes('all') || this.patterns.includes('double_top')) { + if (this.isDoubleTop(prices)) { + patterns.push('double_top'); + } + } + + // Double Bottom + if (this.patterns.includes('all') || this.patterns.includes('double_bottom')) { + if (this.isDoubleBottom(prices)) { + patterns.push('double_bottom'); + } + } + + return patterns; + } + + isHeadAndShoulders(prices) { + if (prices.length < 5) return false; + const recent = prices.slice(-5); + return recent[2] > recent[0] && recent[2] > recent[1] && + recent[2] > recent[3] && recent[2] > recent[4]; + } + + isDoubleTop(prices) { + if (prices.length < 4) return false; + const recent = prices.slice(-4); + return Math.abs(recent[0] - recent[2]) < recent[0] * 0.02 && + recent[1] < recent[0] && recent[3] < recent[2]; + } + + isDoubleBottom(prices) { + if (prices.length < 4) return false; + const recent = prices.slice(-4); + return Math.abs(recent[0] - recent[2]) < recent[0] * 0.02 && + recent[1] > recent[0] && recent[3] > recent[2]; + } +} + +// Portfolio Optimizer +class PortfolioOptimizer { + constructor(config = {}) { + this.riskProfile = config.riskProfile || 'moderate'; + this.maxPositionSize = config.maxPositionSize || 10; + } + + optimize(signals, portfolioValue) { + const allocation = { + positions: [], + totalAllocation: 0, + expectedReturn: 0, + riskScore: 0, + sharpeRatio: 0 + }; + + // Filter high-confidence signals + const validSignals = signals.filter(s => + s.signal !== 'HOLD' && s.confidence >= 70 + ); + + if (validSignals.length === 0) { + return allocation; + } + + // Calculate position sizes using Kelly Criterion + validSignals.forEach(signal => { + const kellyFraction = this.calculateKelly(signal); + const positionSize = Math.min( + kellyFraction * portfolioValue, + (this.maxPositionSize / 100) * portfolioValue + ); + + allocation.positions.push({ + symbol: signal.symbol, + signal: signal.signal, + allocation: positionSize, + percentage: (positionSize / portfolioValue) * 100, + confidence: signal.confidence, + target: signal.target, + stopLoss: signal.stopLoss + }); + + allocation.totalAllocation += positionSize; + }); + + // Calculate portfolio metrics + allocation.expectedReturn = this.calculateExpectedReturn(allocation.positions); + allocation.riskScore = this.calculateRisk(allocation.positions); + allocation.sharpeRatio = allocation.expectedReturn / (allocation.riskScore || 1); + + return allocation; + } + + calculateKelly(signal) { + // Kelly Criterion: f = (bp - q) / b + // where b = odds, p = probability of win, q = probability of loss + const winProb = signal.confidence / 100; + const lossProb = 1 - winProb; + const odds = Math.abs(signal.target - signal.price) / Math.abs(signal.stopLoss - signal.price); + + const kelly = (odds * winProb - lossProb) / odds; + return Math.max(0, Math.min(kelly, 0.25)); // Cap at 25% + } + + calculateExpectedReturn(positions) { + return positions.reduce((sum, pos) => { + const expectedMove = Math.abs(pos.target - pos.stopLoss) / 2; + return sum + (pos.percentage * expectedMove); + }, 0); + } + + calculateRisk(positions) { + // Simple volatility-based risk + const variance = positions.reduce((sum, pos) => { + const risk = Math.abs(pos.stopLoss - pos.target); + return sum + Math.pow(risk * pos.percentage, 2); + }, 0); + return Math.sqrt(variance); + } +} + +// Risk Manager +class RiskManager { + constructor(config = {}) { + this.maxDrawdown = config.maxDrawdown || 20; + this.varConfidence = config.varConfidence || 0.95; + } + + assessRisk(portfolio, marketData) { + const risk = { + valueAtRisk: 0, + expectedShortfall: 0, + maxDrawdown: 0, + positionRisks: [], + recommendations: [] + }; + + // Calculate Value at Risk (VaR) + risk.valueAtRisk = this.calculateVaR(portfolio, marketData); + + // Calculate Expected Shortfall (CVaR) + risk.expectedShortfall = risk.valueAtRisk * 1.5; + + // Assess individual positions + portfolio.positions.forEach(position => { + const positionRisk = { + symbol: position.symbol, + exposure: position.allocation, + riskAmount: Math.abs(position.allocation * + (position.stopLoss - position.target) / position.target), + riskPercentage: ((position.stopLoss - position.target) / position.target) * 100 + }; + risk.positionRisks.push(positionRisk); + + // Generate recommendations + if (positionRisk.riskPercentage > 5) { + risk.recommendations.push( + `Reduce position size for ${position.symbol} - high risk (${positionRisk.riskPercentage.toFixed(2)}%)` + ); + } + }); + + // Portfolio-level recommendations + if (portfolio.totalAllocation > portfolio.value * 0.8) { + risk.recommendations.push('Consider reducing overall exposure - portfolio is highly allocated'); + } + + if (risk.valueAtRisk > portfolio.value * 0.1) { + risk.recommendations.push(`VaR exceeds 10% of portfolio - consider reducing risk`); + } + + return risk; + } + + calculateVaR(portfolio, marketData, confidence = this.varConfidence) { + // Simplified VaR calculation using historical volatility + const returns = marketData.returns || []; + if (returns.length === 0) return 0; + + const sortedReturns = [...returns].sort((a, b) => a - b); + const varIndex = Math.floor((1 - confidence) * sortedReturns.length); + const varReturn = sortedReturns[varIndex]; + + return Math.abs(portfolio.totalAllocation * varReturn); + } +} + +// Swarm Coordinator for multi-agent ensemble +class SwarmCoordinator { + constructor(config = {}) { + this.numAgents = config.swarmAgents || 5; + this.agents = []; + this.initializeAgents(config); + } + + initializeAgents(config) { + for (let i = 0; i < this.numAgents; i++) { + // Create diverse agents with different configurations + const agentConfig = { + ...config.neuralConfig, + learningRate: config.neuralConfig.learningRate * (0.5 + Math.random()), + dropout: config.neuralConfig.dropout * (0.5 + Math.random() * 1.5) + }; + this.agents.push(new NeuralEngine(agentConfig)); + } + } + + predict(input) { + // Get predictions from all agents + const predictions = this.agents.map(agent => { + const output = agent.forward(input); + return output[0]; // Get first output (prediction) + }); + + // Consensus voting with weighted average + const weights = predictions.map((_, i) => 1 / this.numAgents); + const consensus = predictions.reduce((sum, pred, i) => + sum + pred * weights[i], 0 + ); + + return { + consensus, + predictions, + agreement: 1 - this.calculateVariance(predictions), + individual: predictions + }; + } + + calculateVariance(predictions) { + const mean = predictions.reduce((a, b) => a + b, 0) / predictions.length; + const variance = predictions.reduce((sum, p) => + sum + Math.pow(p - mean, 2), 0) / predictions.length; + return Math.sqrt(variance); + } +} + +// Technical Indicators +class TechnicalIndicators { + static calculateRSI(prices, period = 14) { + if (prices.length < period + 1) return 50; + + const changes = prices.slice(1).map((price, i) => price - prices[i]); + const gains = changes.map(c => c > 0 ? c : 0); + const losses = changes.map(c => c < 0 ? -c : 0); + + const avgGain = gains.slice(-period).reduce((a, b) => a + b, 0) / period; + const avgLoss = losses.slice(-period).reduce((a, b) => a + b, 0) / period; + + if (avgLoss === 0) return 100; + const rs = avgGain / avgLoss; + return 100 - (100 / (1 + rs)); + } + + static calculateMACD(prices, fast = 12, slow = 26, signal = 9) { + const emaFast = this.calculateEMA(prices, fast); + const emaSlow = this.calculateEMA(prices, slow); + const macdLine = emaFast - emaSlow; + + return { + macd: macdLine, + signal: this.calculateEMA([macdLine], signal), + histogram: macdLine - this.calculateEMA([macdLine], signal) + }; + } + + static calculateEMA(prices, period) { + if (prices.length === 0) return 0; + const k = 2 / (period + 1); + let ema = prices[0]; + + for (let i = 1; i < prices.length; i++) { + ema = prices[i] * k + ema * (1 - k); + } + + return ema; + } + + static calculateBollinger(prices, period = 20, stdDev = 2) { + const sma = prices.slice(-period).reduce((a, b) => a + b, 0) / period; + const variance = prices.slice(-period) + .reduce((sum, p) => sum + Math.pow(p - sma, 2), 0) / period; + const std = Math.sqrt(variance); + + return { + upper: sma + stdDev * std, + middle: sma, + lower: sma - stdDev * std + }; + } + + static calculateATR(highs, lows, closes, period = 14) { + const trs = []; + for (let i = 1; i < closes.length; i++) { + const tr = Math.max( + highs[i] - lows[i], + Math.abs(highs[i] - closes[i - 1]), + Math.abs(lows[i] - closes[i - 1]) + ); + trs.push(tr); + } + return trs.slice(-period).reduce((a, b) => a + b, 0) / period; + } +} + +// Main Actor +await Actor.main(async () => { + console.log('šŸš€ Neural Trader System - Starting...'); + + const input = await Actor.getInput(); + const { + mode = 'signals', + symbols = ['BTC/USD'], + strategy = 'ensemble', + riskProfile = 'moderate', + maxPositionSize = 10, + stopLoss = 2.5, + takeProfit = 5, + timeframe = '1h', + lookbackPeriod = 100, + neuralConfig = {}, + enableSwarm = true, + swarmAgents = 5, + outputFormat = 'full_analysis', + webhookUrl = null, + backtestDays = 30, + enableGpu = true, + confidenceThreshold = 70, + patterns = ['all'], + indicators = {} + } = input; + + console.log(`šŸ“Š Mode: ${mode}`); + console.log(`šŸ’¹ Symbols: ${symbols.join(', ')}`); + console.log(`🧠 Strategy: ${strategy}`); + console.log(`šŸŽÆ Risk Profile: ${riskProfile}`); + + // Initialize components + const neuralEngine = new NeuralEngine(neuralConfig); + const signalGenerator = new SignalGenerator({ confidenceThreshold, patterns }); + const portfolioOptimizer = new PortfolioOptimizer({ riskProfile, maxPositionSize }); + const riskManager = new RiskManager(); + const swarmCoordinator = enableSwarm ? new SwarmCoordinator({ swarmAgents, neuralConfig }) : null; + + const results = []; + + // Process each symbol + for (const symbol of symbols) { + console.log(`\nšŸ“ˆ Analyzing ${symbol}...`); + + // Generate synthetic market data (in production, fetch real data) + const marketData = generateMarketData(symbol, lookbackPeriod, { + stopLoss, + takeProfit, + timeframe + }); + + // Calculate technical indicators + const technicalData = { + rsi: indicators.rsi ? TechnicalIndicators.calculateRSI(marketData.prices) : null, + macd: indicators.macd ? TechnicalIndicators.calculateMACD(marketData.prices) : null, + bollinger: indicators.bollinger ? TechnicalIndicators.calculateBollinger(marketData.prices) : null, + atr: indicators.atr ? TechnicalIndicators.calculateATR( + marketData.highs, marketData.lows, marketData.prices + ) : null + }; + + // Prepare neural network input + const features = prepareFeatures(marketData, technicalData); + + // Get predictions + let predictions; + if (enableSwarm && swarmCoordinator) { + const swarmResult = swarmCoordinator.predict(features); + predictions = swarmResult.individual; + console.log(`šŸ¤– Swarm consensus: ${(swarmResult.consensus * 100).toFixed(2)}%`); + console.log(`šŸŽÆ Agreement: ${(swarmResult.agreement * 100).toFixed(2)}%`); + } else { + const output = neuralEngine.forward(features); + predictions = [output[0]]; + } + + // Generate trading signal + const signal = signalGenerator.generateSignal(predictions, marketData); + + console.log(`${signal.signal === 'BUY' ? '🟢' : signal.signal === 'SELL' ? 'šŸ”“' : '⚪'} Signal: ${signal.signal}`); + console.log(`šŸ’Ŗ Confidence: ${signal.confidence.toFixed(2)}%`); + + // Create result object + const result = { + ...signal, + technical: technicalData, + prediction: predictions.reduce((a, b) => a + b, 0) / predictions.length, + swarmPredictions: enableSwarm ? predictions : null, + timeframe, + strategy + }; + + results.push(result); + + // Push to dataset + await Actor.pushData(result); + } + + // Portfolio optimization + if (mode === 'optimize' || outputFormat === 'portfolio') { + console.log('\nšŸ’¼ Optimizing portfolio...'); + + const portfolioValue = 100000; // Example portfolio value + const portfolio = portfolioOptimizer.optimize(results, portfolioValue); + + console.log(`šŸ“Š Total Allocation: $${portfolio.totalAllocation.toFixed(2)}`); + console.log(`šŸ“ˆ Expected Return: ${portfolio.expectedReturn.toFixed(2)}%`); + console.log(`āš ļø Risk Score: ${portfolio.riskScore.toFixed(2)}`); + console.log(`šŸ“‰ Sharpe Ratio: ${portfolio.sharpeRatio.toFixed(2)}`); + + // Risk assessment + const risk = riskManager.assessRisk( + { ...portfolio, value: portfolioValue }, + { returns: generateReturns(lookbackPeriod) } + ); + + console.log(`\nšŸ›”ļø Risk Assessment:`); + console.log(`šŸ’° Value at Risk (95%): $${risk.valueAtRisk.toFixed(2)}`); + console.log(`šŸ“‰ Expected Shortfall: $${risk.expectedShortfall.toFixed(2)}`); + + if (risk.recommendations.length > 0) { + console.log(`\nšŸ’” Recommendations:`); + risk.recommendations.forEach(rec => console.log(` • ${rec}`)); + } + + await Actor.pushData({ + type: 'portfolio', + portfolio, + risk, + timestamp: new Date().toISOString() + }); + } + + // Send webhook if configured + if (webhookUrl && results.length > 0) { + console.log(`\nšŸ”” Sending webhook to ${webhookUrl}...`); + try { + await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + signals: results, + timestamp: new Date().toISOString(), + strategy, + mode + }) + }); + console.log('āœ… Webhook sent successfully'); + } catch (error) { + console.error('āŒ Webhook failed:', error.message); + } + } + + console.log(`\nāœ… Neural Trader System completed`); + console.log(`šŸ“Š Processed ${symbols.length} symbols`); + console.log(`šŸŽÆ Generated ${results.filter(r => r.signal !== 'HOLD').length} signals`); +}); + +// Helper functions +function generateMarketData(symbol, periods, config) { + const prices = []; + const highs = []; + const lows = []; + const volumes = []; + + let price = 100 + Math.random() * 900; // Random starting price + + for (let i = 0; i < periods; i++) { + const change = (Math.random() - 0.5) * price * 0.03; // 3% max change + price += change; + + prices.push(price); + highs.push(price * (1 + Math.random() * 0.01)); + lows.push(price * (1 - Math.random() * 0.01)); + volumes.push(Math.random() * 1000000); + } + + return { + symbol, + price: prices[prices.length - 1], + prices, + highs, + lows, + volumes, + stopLoss: config.stopLoss, + takeProfit: config.takeProfit, + timeframe: config.timeframe + }; +} + +function prepareFeatures(marketData, technicalData) { + const features = []; + + // Price features (normalized) + const prices = marketData.prices.slice(-20); + const priceNorm = prices.map(p => p / marketData.price); + features.push(...priceNorm); + + // Technical indicators + if (technicalData.rsi !== null) { + features.push(technicalData.rsi / 100); + } + + if (technicalData.macd !== null) { + features.push( + technicalData.macd.macd / 100, + technicalData.macd.signal / 100, + technicalData.macd.histogram / 100 + ); + } + + if (technicalData.bollinger !== null) { + features.push( + technicalData.bollinger.upper / marketData.price, + technicalData.bollinger.middle / marketData.price, + technicalData.bollinger.lower / marketData.price + ); + } + + // Pad to 50 features + while (features.length < 50) { + features.push(0); + } + + return features.slice(0, 50); +} + +function generateReturns(periods) { + const returns = []; + for (let i = 0; i < periods; i++) { + // Generate random returns with normal distribution + returns.push((Math.random() - 0.5) * 0.05); + } + return returns; +} diff --git a/npm/core/platforms/linux-x64-gnu/package.json b/npm/core/platforms/linux-x64-gnu/package.json index f0c38f10..4d8ea972 100644 --- a/npm/core/platforms/linux-x64-gnu/package.json +++ b/npm/core/platforms/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "ruvector-core-linux-x64-gnu", - "version": "0.1.25", + "version": "0.1.26", "description": "Linux x64 GNU native binding for ruvector-core - High-performance vector database with HNSW indexing built in Rust", "main": "index.js", "type": "commonjs", diff --git a/npm/packages/core/package.json b/npm/packages/core/package.json index 409461dd..b2d8ef6c 100644 --- a/npm/packages/core/package.json +++ b/npm/packages/core/package.json @@ -1,6 +1,6 @@ { - "name": "ruvector-core", - "version": "0.1.26", + "name": "@ruvector/core", + "version": "0.1.28", "description": "High-performance vector database with HNSW indexing - 50k+ inserts/sec, built in Rust for AI/ML similarity search and semantic search applications", "main": "index.js", "types": "index.d.ts", @@ -32,7 +32,7 @@ "@napi-rs/cli": "^2.18.0" }, "optionalDependencies": { - "ruvector-core-linux-x64-gnu": "0.1.25", + "ruvector-core-linux-x64-gnu": "0.1.26", "ruvector-core-linux-arm64-gnu": "0.1.25", "ruvector-core-darwin-x64": "0.1.25", "ruvector-core-darwin-arm64": "0.1.25", diff --git a/npm/packages/ruvector/package.json b/npm/packages/ruvector/package.json index bc618045..9abb4416 100644 --- a/npm/packages/ruvector/package.json +++ b/npm/packages/ruvector/package.json @@ -1,6 +1,6 @@ { "name": "ruvector", - "version": "0.1.33", + "version": "0.1.35", "description": "High-performance vector database for Node.js with automatic native/WASM fallback", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/npm/packages/ruvector/src/index.ts b/npm/packages/ruvector/src/index.ts index 519711bc..5c908ced 100644 --- a/npm/packages/ruvector/src/index.ts +++ b/npm/packages/ruvector/src/index.ts @@ -73,8 +73,114 @@ export function getVersion(): { version: string; implementation: string } { }; } -// Export the VectorDB class -export const VectorDB = implementation.VectorDB; +/** + * Wrapper class that automatically handles metadata JSON conversion + */ +class VectorDBWrapper { + private db: any; + + constructor(options: { dimensions: number; storagePath?: string; distanceMetric?: string; hnswConfig?: any }) { + this.db = new implementation.VectorDb(options); + } + + /** + * Insert a vector with optional metadata (objects are auto-converted to JSON) + */ + async insert(entry: { id?: string; vector: Float32Array | number[]; metadata?: Record }): Promise { + const nativeEntry: any = { + id: entry.id, + vector: entry.vector instanceof Float32Array ? entry.vector : new Float32Array(entry.vector), + }; + + // Auto-convert metadata object to JSON string + if (entry.metadata) { + nativeEntry.metadata = JSON.stringify(entry.metadata); + } + + return this.db.insert(nativeEntry); + } + + /** + * Insert multiple vectors in batch + */ + async insertBatch(entries: Array<{ id?: string; vector: Float32Array | number[]; metadata?: Record }>): Promise { + const nativeEntries = entries.map(entry => ({ + id: entry.id, + vector: entry.vector instanceof Float32Array ? entry.vector : new Float32Array(entry.vector), + metadata: entry.metadata ? JSON.stringify(entry.metadata) : undefined, + })); + + return this.db.insertBatch(nativeEntries); + } + + /** + * Search for similar vectors (metadata is auto-parsed from JSON) + */ + async search(query: { vector: Float32Array | number[]; k: number; filter?: Record; efSearch?: number }): Promise }>> { + const nativeQuery: any = { + vector: query.vector instanceof Float32Array ? query.vector : new Float32Array(query.vector), + k: query.k, + efSearch: query.efSearch, + }; + + // Auto-convert filter object to JSON string + if (query.filter) { + nativeQuery.filter = JSON.stringify(query.filter); + } + + const results = await this.db.search(nativeQuery); + + // Auto-parse metadata JSON strings back to objects + return results.map((r: any) => ({ + id: r.id, + score: r.score, + vector: r.vector, + metadata: r.metadata ? JSON.parse(r.metadata) : undefined, + })); + } + + /** + * Get a vector by ID (metadata is auto-parsed from JSON) + */ + async get(id: string): Promise<{ id?: string; vector: Float32Array; metadata?: Record } | null> { + const entry = await this.db.get(id); + if (!entry) return null; + + return { + id: entry.id, + vector: entry.vector, + metadata: entry.metadata ? JSON.parse(entry.metadata) : undefined, + }; + } + + /** + * Delete a vector by ID + */ + async delete(id: string): Promise { + return this.db.delete(id); + } + + /** + * Get the number of vectors in the database + */ + async len(): Promise { + return this.db.len(); + } + + /** + * Check if the database is empty + */ + async isEmpty(): Promise { + return this.db.isEmpty(); + } +} + +// Export the wrapper class (aliased as VectorDB for backwards compatibility) +export const VectorDb = VectorDBWrapper; +export const VectorDB = VectorDBWrapper; + +// Also export the raw native implementation for advanced users +export const NativeVectorDb = implementation.VectorDb; // Export everything from the implementation export default implementation;