Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
541
vendor/ruvector/examples/neural-trader/tests/neural-trader.test.js
vendored
Normal file
541
vendor/ruvector/examples/neural-trader/tests/neural-trader.test.js
vendored
Normal file
@@ -0,0 +1,541 @@
|
||||
/**
|
||||
* Neural-Trader Test Suite
|
||||
*
|
||||
* Comprehensive tests for all production modules
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from '@jest/globals';
|
||||
|
||||
// Mock performance for Node.js
|
||||
if (typeof performance === 'undefined') {
|
||||
global.performance = { now: () => Date.now() };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FRACTIONAL KELLY TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Fractional Kelly Engine', () => {
|
||||
let KellyCriterion, TradingKelly, SportsBettingKelly;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await import('../production/fractional-kelly.js');
|
||||
KellyCriterion = module.KellyCriterion;
|
||||
TradingKelly = module.TradingKelly;
|
||||
SportsBettingKelly = module.SportsBettingKelly;
|
||||
});
|
||||
|
||||
describe('KellyCriterion', () => {
|
||||
it('should calculate full Kelly correctly', () => {
|
||||
const kelly = new KellyCriterion();
|
||||
const result = kelly.calculateFullKelly(0.55, 2.0);
|
||||
expect(result).toBeCloseTo(0.10, 2); // 10% for 55% win rate at even odds
|
||||
});
|
||||
|
||||
it('should return 0 for negative edge', () => {
|
||||
const kelly = new KellyCriterion();
|
||||
const result = kelly.calculateFullKelly(0.40, 2.0);
|
||||
expect(result).toBeLessThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should apply fractional Kelly correctly', () => {
|
||||
const kelly = new KellyCriterion();
|
||||
const result = kelly.calculateFractionalKelly(0.55, 2.0, 'conservative');
|
||||
expect(result.stake).toBeLessThan(kelly.calculateFullKelly(0.55, 2.0) * 10000);
|
||||
});
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
const kelly = new KellyCriterion();
|
||||
expect(kelly.calculateFullKelly(0, 2.0)).toBeLessThanOrEqual(0);
|
||||
expect(kelly.calculateFullKelly(1, 2.0)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TradingKelly', () => {
|
||||
it('should calculate position size', () => {
|
||||
const kelly = new TradingKelly();
|
||||
const result = kelly.calculatePositionSize(100000, 0.55, 0.02, 0.015);
|
||||
expect(result.positionSize).toBeGreaterThan(0);
|
||||
expect(result.positionSize).toBeLessThan(100000);
|
||||
});
|
||||
|
||||
it('should respect max position limits', () => {
|
||||
const kelly = new TradingKelly();
|
||||
const result = kelly.calculatePositionSize(100000, 0.99, 0.10, 0.01);
|
||||
expect(result.positionSize).toBeLessThanOrEqual(100000 * 0.20);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// LSTM-TRANSFORMER TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Hybrid LSTM-Transformer', () => {
|
||||
let HybridLSTMTransformer, FeatureExtractor, LSTMCell;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await import('../production/hybrid-lstm-transformer.js');
|
||||
HybridLSTMTransformer = module.HybridLSTMTransformer;
|
||||
FeatureExtractor = module.FeatureExtractor;
|
||||
LSTMCell = module.LSTMCell;
|
||||
});
|
||||
|
||||
describe('LSTMCell', () => {
|
||||
it('should forward pass correctly', () => {
|
||||
const cell = new LSTMCell(10, 64);
|
||||
const x = new Array(10).fill(0.1);
|
||||
const h = new Array(64).fill(0);
|
||||
const c = new Array(64).fill(0);
|
||||
|
||||
const result = cell.forward(x, h, c);
|
||||
expect(result.h).toHaveLength(64);
|
||||
expect(result.c).toHaveLength(64);
|
||||
expect(result.h.every(v => !isNaN(v))).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle zero inputs', () => {
|
||||
const cell = new LSTMCell(10, 64);
|
||||
const x = new Array(10).fill(0);
|
||||
const h = new Array(64).fill(0);
|
||||
const c = new Array(64).fill(0);
|
||||
|
||||
const result = cell.forward(x, h, c);
|
||||
expect(result.h.every(v => isFinite(v))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FeatureExtractor', () => {
|
||||
it('should extract features from candles', () => {
|
||||
const extractor = new FeatureExtractor();
|
||||
const candles = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
candles.push({
|
||||
open: 100 + Math.random() * 10,
|
||||
high: 105 + Math.random() * 10,
|
||||
low: 95 + Math.random() * 10,
|
||||
close: 100 + Math.random() * 10,
|
||||
volume: 1000000
|
||||
});
|
||||
}
|
||||
|
||||
const features = extractor.extract(candles);
|
||||
expect(features.length).toBe(99); // One less than candles
|
||||
expect(features[0].length).toBe(10); // 10 features
|
||||
});
|
||||
});
|
||||
|
||||
describe('HybridLSTMTransformer', () => {
|
||||
it('should produce valid predictions', () => {
|
||||
const model = new HybridLSTMTransformer();
|
||||
const features = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
features.push(new Array(10).fill(0).map(() => Math.random() - 0.5));
|
||||
}
|
||||
|
||||
const result = model.predict(features);
|
||||
expect(result).toHaveProperty('prediction');
|
||||
expect(result).toHaveProperty('confidence');
|
||||
expect(result).toHaveProperty('signal');
|
||||
expect(['BUY', 'SELL', 'HOLD']).toContain(result.signal);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// DRL PORTFOLIO MANAGER TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('DRL Portfolio Manager', () => {
|
||||
let EnsemblePortfolioManager, ReplayBuffer, NeuralNetwork;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await import('../production/drl-portfolio-manager.js');
|
||||
EnsemblePortfolioManager = module.EnsemblePortfolioManager;
|
||||
ReplayBuffer = module.ReplayBuffer;
|
||||
NeuralNetwork = module.NeuralNetwork;
|
||||
});
|
||||
|
||||
describe('ReplayBuffer', () => {
|
||||
it('should store and sample experiences', () => {
|
||||
const buffer = new ReplayBuffer(100);
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
buffer.push(
|
||||
new Array(10).fill(i),
|
||||
new Array(5).fill(0.2),
|
||||
Math.random(),
|
||||
new Array(10).fill(i + 1),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
expect(buffer.length).toBe(50);
|
||||
const batch = buffer.sample(16);
|
||||
expect(batch).toHaveLength(16);
|
||||
});
|
||||
|
||||
it('should respect max size', () => {
|
||||
const buffer = new ReplayBuffer(10);
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
buffer.push([i], [0.5], 1, [i + 1], false);
|
||||
}
|
||||
|
||||
expect(buffer.length).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NeuralNetwork', () => {
|
||||
it('should forward pass correctly', () => {
|
||||
const net = new NeuralNetwork([10, 32, 5]);
|
||||
const input = new Array(10).fill(0.5);
|
||||
const result = net.forward(input);
|
||||
|
||||
expect(result.output).toHaveLength(5);
|
||||
expect(result.output.every(v => isFinite(v))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EnsemblePortfolioManager', () => {
|
||||
it('should produce valid portfolio weights', () => {
|
||||
const manager = new EnsemblePortfolioManager(5);
|
||||
const state = new Array(62).fill(0).map(() => Math.random());
|
||||
const action = manager.getEnsembleAction(state);
|
||||
|
||||
expect(action).toHaveLength(5);
|
||||
const sum = action.reduce((a, b) => a + b, 0);
|
||||
expect(sum).toBeCloseTo(1, 1); // Weights should sum to ~1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// SENTIMENT ALPHA TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Sentiment Alpha Pipeline', () => {
|
||||
let LexiconAnalyzer, EmbeddingAnalyzer, SentimentAggregator;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await import('../production/sentiment-alpha.js');
|
||||
LexiconAnalyzer = module.LexiconAnalyzer;
|
||||
EmbeddingAnalyzer = module.EmbeddingAnalyzer;
|
||||
SentimentAggregator = module.SentimentAggregator;
|
||||
});
|
||||
|
||||
describe('LexiconAnalyzer', () => {
|
||||
it('should detect positive sentiment', () => {
|
||||
const analyzer = new LexiconAnalyzer();
|
||||
const result = analyzer.analyze('Strong growth and profit beat expectations');
|
||||
expect(result.score).toBeGreaterThan(0);
|
||||
expect(result.positiveCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should detect negative sentiment', () => {
|
||||
const analyzer = new LexiconAnalyzer();
|
||||
const result = analyzer.analyze('Losses and decline amid recession fears');
|
||||
expect(result.score).toBeLessThan(0);
|
||||
expect(result.negativeCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle neutral text', () => {
|
||||
const analyzer = new LexiconAnalyzer();
|
||||
const result = analyzer.analyze('The company released quarterly results');
|
||||
expect(Math.abs(result.score)).toBeLessThan(0.5);
|
||||
});
|
||||
|
||||
it('should handle negators', () => {
|
||||
const analyzer = new LexiconAnalyzer();
|
||||
const positive = analyzer.analyze('Strong growth');
|
||||
const negated = analyzer.analyze('Not strong growth');
|
||||
expect(negated.score).toBeLessThan(positive.score);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SentimentAggregator', () => {
|
||||
it('should aggregate multiple sentiments', () => {
|
||||
const aggregator = new SentimentAggregator();
|
||||
|
||||
aggregator.addSentiment('AAPL', { source: 'news', score: 0.5, confidence: 0.8 });
|
||||
aggregator.addSentiment('AAPL', { source: 'social', score: 0.3, confidence: 0.6 });
|
||||
|
||||
const result = aggregator.getAggregatedSentiment('AAPL');
|
||||
expect(result.score).toBeGreaterThan(0);
|
||||
expect(result.count).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// BACKTESTING TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Backtesting Framework', () => {
|
||||
let PerformanceMetrics, BacktestEngine;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await import('../system/backtesting.js');
|
||||
PerformanceMetrics = module.PerformanceMetrics;
|
||||
BacktestEngine = module.BacktestEngine;
|
||||
});
|
||||
|
||||
describe('PerformanceMetrics', () => {
|
||||
it('should calculate metrics from equity curve', () => {
|
||||
const metrics = new PerformanceMetrics(0.05);
|
||||
const equityCurve = [100000];
|
||||
|
||||
// Generate random walk
|
||||
for (let i = 0; i < 252; i++) {
|
||||
const lastValue = equityCurve[equityCurve.length - 1];
|
||||
equityCurve.push(lastValue * (1 + (Math.random() - 0.48) * 0.02));
|
||||
}
|
||||
|
||||
const result = metrics.calculate(equityCurve);
|
||||
|
||||
expect(result).toHaveProperty('totalReturn');
|
||||
expect(result).toHaveProperty('sharpeRatio');
|
||||
expect(result).toHaveProperty('maxDrawdown');
|
||||
expect(result.tradingDays).toBe(252);
|
||||
});
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
const metrics = new PerformanceMetrics();
|
||||
|
||||
// Empty curve
|
||||
expect(metrics.calculate([]).totalReturn).toBe(0);
|
||||
|
||||
// Single value
|
||||
expect(metrics.calculate([100]).totalReturn).toBe(0);
|
||||
});
|
||||
|
||||
it('should compute drawdown correctly', () => {
|
||||
const metrics = new PerformanceMetrics();
|
||||
const equityCurve = [100, 110, 105, 95, 100, 90, 95];
|
||||
|
||||
const result = metrics.calculate(equityCurve);
|
||||
expect(result.maxDrawdown).toBeCloseTo(0.182, 2); // (110-90)/110
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// RISK MANAGEMENT TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Risk Management', () => {
|
||||
let RiskManager, CircuitBreaker, StopLossManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await import('../system/risk-management.js');
|
||||
RiskManager = module.RiskManager;
|
||||
CircuitBreaker = module.CircuitBreaker;
|
||||
StopLossManager = module.StopLossManager;
|
||||
});
|
||||
|
||||
describe('StopLossManager', () => {
|
||||
it('should set and check stops', () => {
|
||||
const manager = new StopLossManager();
|
||||
manager.setStop('AAPL', 150, 'fixed');
|
||||
|
||||
const check = manager.checkStop('AAPL', 140);
|
||||
expect(check.triggered).toBe(true);
|
||||
});
|
||||
|
||||
it('should update trailing stops', () => {
|
||||
const manager = new StopLossManager();
|
||||
manager.setStop('AAPL', 100, 'trailing');
|
||||
|
||||
manager.updateTrailingStop('AAPL', 110);
|
||||
const stop = manager.stops.get('AAPL');
|
||||
expect(stop.highWaterMark).toBe(110);
|
||||
expect(stop.stopPrice).toBeGreaterThan(100 * 0.97);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CircuitBreaker', () => {
|
||||
it('should trip on consecutive losses', () => {
|
||||
const breaker = new CircuitBreaker({ consecutiveLosses: 3 });
|
||||
|
||||
breaker.recordTrade(-100);
|
||||
breaker.recordTrade(-100);
|
||||
expect(breaker.canTrade().allowed).toBe(true);
|
||||
|
||||
breaker.recordTrade(-100);
|
||||
expect(breaker.canTrade().allowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should reset after cooldown', () => {
|
||||
const breaker = new CircuitBreaker({
|
||||
consecutiveLosses: 2,
|
||||
drawdownCooldown: 100 // 100ms for testing
|
||||
});
|
||||
|
||||
breaker.recordTrade(-100);
|
||||
breaker.recordTrade(-100);
|
||||
expect(breaker.canTrade().allowed).toBe(false);
|
||||
|
||||
// Wait for cooldown
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(breaker.canTrade().allowed).toBe(true);
|
||||
resolve();
|
||||
}, 150);
|
||||
});
|
||||
});
|
||||
|
||||
it('should trip on drawdown', () => {
|
||||
const breaker = new CircuitBreaker({ drawdownThreshold: 0.10 });
|
||||
|
||||
breaker.updateEquity(100000);
|
||||
breaker.updateEquity(88000); // 12% drawdown
|
||||
|
||||
expect(breaker.canTrade().allowed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RiskManager', () => {
|
||||
it('should check trade limits', () => {
|
||||
const manager = new RiskManager();
|
||||
manager.startDay(100000);
|
||||
|
||||
const portfolio = {
|
||||
equity: 100000,
|
||||
cash: 50000,
|
||||
positions: {}
|
||||
};
|
||||
|
||||
const trade = { symbol: 'AAPL', side: 'buy', value: 5000 };
|
||||
const result = manager.canTrade('AAPL', trade, portfolio);
|
||||
|
||||
expect(result.allowed).toBe(true);
|
||||
});
|
||||
|
||||
it('should block oversized trades', () => {
|
||||
const manager = new RiskManager();
|
||||
|
||||
const portfolio = {
|
||||
equity: 100000,
|
||||
cash: 50000,
|
||||
positions: {}
|
||||
};
|
||||
|
||||
const trade = { symbol: 'AAPL', side: 'buy', value: 60000 };
|
||||
const result = manager.canTrade('AAPL', trade, portfolio);
|
||||
|
||||
expect(result.checks.positionSize.violations.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// TRADING PIPELINE TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Trading Pipeline', () => {
|
||||
let TradingPipeline, TradingDag;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await import('../system/trading-pipeline.js');
|
||||
TradingPipeline = module.TradingPipeline;
|
||||
TradingDag = module.TradingDag;
|
||||
});
|
||||
|
||||
describe('TradingDag', () => {
|
||||
it('should execute nodes in order', async () => {
|
||||
const dag = new TradingDag();
|
||||
const order = [];
|
||||
|
||||
dag.addNode({
|
||||
id: 'a',
|
||||
name: 'Node A',
|
||||
dependencies: [],
|
||||
status: 'pending',
|
||||
executor: async () => { order.push('a'); return 'A'; }
|
||||
});
|
||||
|
||||
dag.addNode({
|
||||
id: 'b',
|
||||
name: 'Node B',
|
||||
dependencies: ['a'],
|
||||
status: 'pending',
|
||||
executor: async (ctx, deps) => { order.push('b'); return deps.a + 'B'; }
|
||||
});
|
||||
|
||||
await dag.execute({});
|
||||
|
||||
expect(order).toEqual(['a', 'b']);
|
||||
expect(dag.results.get('b')).toBe('AB');
|
||||
});
|
||||
|
||||
it('should execute parallel nodes concurrently', async () => {
|
||||
const dag = new TradingDag({ parallelExecution: true, maxConcurrency: 2 });
|
||||
const timestamps = {};
|
||||
|
||||
dag.addNode({
|
||||
id: 'a',
|
||||
dependencies: [],
|
||||
status: 'pending',
|
||||
executor: async () => { timestamps.a = Date.now(); return 'A'; }
|
||||
});
|
||||
|
||||
dag.addNode({
|
||||
id: 'b',
|
||||
dependencies: [],
|
||||
status: 'pending',
|
||||
executor: async () => { timestamps.b = Date.now(); return 'B'; }
|
||||
});
|
||||
|
||||
await dag.execute({});
|
||||
|
||||
// Both should start at nearly the same time
|
||||
expect(Math.abs(timestamps.a - timestamps.b)).toBeLessThan(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// INTEGRATION TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
it('should run complete pipeline', async () => {
|
||||
const { createTradingPipeline } = await import('../system/trading-pipeline.js');
|
||||
const pipeline = createTradingPipeline();
|
||||
|
||||
const generateCandles = (n) => {
|
||||
const candles = [];
|
||||
let price = 100;
|
||||
for (let i = 0; i < n; i++) {
|
||||
price *= (1 + (Math.random() - 0.5) * 0.02);
|
||||
candles.push({
|
||||
open: price * 0.99,
|
||||
high: price * 1.01,
|
||||
low: price * 0.98,
|
||||
close: price,
|
||||
volume: 1000000
|
||||
});
|
||||
}
|
||||
return candles;
|
||||
};
|
||||
|
||||
const context = {
|
||||
marketData: generateCandles(100),
|
||||
newsData: [
|
||||
{ symbol: 'TEST', text: 'Strong earnings growth', source: 'news' }
|
||||
],
|
||||
symbols: ['TEST'],
|
||||
portfolio: { equity: 100000, cash: 50000, positions: {}, assets: ['TEST'] },
|
||||
prices: { TEST: 100 }
|
||||
};
|
||||
|
||||
const result = await pipeline.execute(context);
|
||||
|
||||
expect(result).toHaveProperty('signals');
|
||||
expect(result).toHaveProperty('positions');
|
||||
expect(result).toHaveProperty('orders');
|
||||
expect(result).toHaveProperty('metrics');
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
Reference in New Issue
Block a user