Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
652
vendor/ruvector/examples/neural-trader/system/backtesting.js
vendored
Normal file
652
vendor/ruvector/examples/neural-trader/system/backtesting.js
vendored
Normal file
@@ -0,0 +1,652 @@
|
||||
/**
|
||||
* Backtesting Framework
|
||||
*
|
||||
* Historical simulation with comprehensive performance metrics:
|
||||
* - Sharpe Ratio, Sortino Ratio
|
||||
* - Maximum Drawdown, Calmar Ratio
|
||||
* - Win Rate, Profit Factor
|
||||
* - Value at Risk (VaR), Expected Shortfall
|
||||
* - Rolling statistics and regime analysis
|
||||
*/
|
||||
|
||||
import { TradingPipeline, createTradingPipeline } from './trading-pipeline.js';
|
||||
|
||||
// Backtesting Configuration
|
||||
const backtestConfig = {
|
||||
// Simulation settings
|
||||
simulation: {
|
||||
initialCapital: 100000,
|
||||
startDate: null, // Use all available data if null
|
||||
endDate: null,
|
||||
rebalanceFrequency: 'daily', // daily, weekly, monthly
|
||||
warmupPeriod: 50 // Days for indicator warmup
|
||||
},
|
||||
|
||||
// Execution assumptions
|
||||
execution: {
|
||||
slippage: 0.001, // 0.1%
|
||||
commission: 0.001, // 0.1%
|
||||
marketImpact: 0.0005, // 0.05% for large orders
|
||||
fillRate: 1.0 // 100% fill rate assumed
|
||||
},
|
||||
|
||||
// Risk-free rate for Sharpe calculation
|
||||
riskFreeRate: 0.05, // 5% annual
|
||||
|
||||
// Benchmark
|
||||
benchmark: 'buyAndHold' // buyAndHold, equalWeight, or custom
|
||||
};
|
||||
|
||||
/**
|
||||
* Performance Metrics Calculator
|
||||
*/
|
||||
class PerformanceMetrics {
|
||||
constructor(riskFreeRate = 0.05) {
|
||||
this.riskFreeRate = riskFreeRate;
|
||||
this.dailyRiskFreeRate = Math.pow(1 + riskFreeRate, 1/252) - 1;
|
||||
}
|
||||
|
||||
// Optimized: Calculate all metrics with minimal passes over data
|
||||
calculate(equityCurve, benchmark = null) {
|
||||
if (equityCurve.length < 2) {
|
||||
return this.emptyMetrics();
|
||||
}
|
||||
|
||||
// Single pass: compute returns and statistics together
|
||||
const n = equityCurve.length;
|
||||
const returns = new Array(n - 1);
|
||||
let sum = 0, sumSq = 0;
|
||||
let positiveSum = 0, negativeSum = 0;
|
||||
let positiveCount = 0, negativeCount = 0;
|
||||
let compoundReturn = 1;
|
||||
|
||||
for (let i = 1; i < n; i++) {
|
||||
const r = (equityCurve[i] - equityCurve[i-1]) / equityCurve[i-1];
|
||||
returns[i-1] = r;
|
||||
sum += r;
|
||||
sumSq += r * r;
|
||||
compoundReturn *= (1 + r);
|
||||
if (r > 0) { positiveSum += r; positiveCount++; }
|
||||
else if (r < 0) { negativeSum += r; negativeCount++; }
|
||||
}
|
||||
|
||||
const mean = sum / returns.length;
|
||||
const variance = sumSq / returns.length - mean * mean;
|
||||
const volatility = Math.sqrt(variance);
|
||||
const annualizedVol = volatility * Math.sqrt(252);
|
||||
|
||||
// Single pass: drawdown metrics
|
||||
const ddMetrics = this.computeDrawdownMetrics(equityCurve);
|
||||
|
||||
// Pre-computed stats for Sharpe/Sortino
|
||||
const excessMean = mean - this.dailyRiskFreeRate;
|
||||
const sharpe = volatility > 0 ? (excessMean / volatility) * Math.sqrt(252) : 0;
|
||||
|
||||
// Downside deviation (single pass)
|
||||
let downsideVariance = 0;
|
||||
for (let i = 0; i < returns.length; i++) {
|
||||
const excess = returns[i] - this.dailyRiskFreeRate;
|
||||
if (excess < 0) downsideVariance += excess * excess;
|
||||
}
|
||||
const downsideDeviation = Math.sqrt(downsideVariance / returns.length);
|
||||
const sortino = downsideDeviation > 0 ? (excessMean / downsideDeviation) * Math.sqrt(252) : 0;
|
||||
|
||||
// Annualized return
|
||||
const years = returns.length / 252;
|
||||
const annualizedReturn = Math.pow(compoundReturn, 1 / years) - 1;
|
||||
|
||||
// CAGR
|
||||
const cagr = Math.pow(equityCurve[n-1] / equityCurve[0], 1 / years) - 1;
|
||||
|
||||
// Calmar
|
||||
const calmar = ddMetrics.maxDrawdown > 0 ? annualizedReturn / ddMetrics.maxDrawdown : 0;
|
||||
|
||||
// Trade metrics (using pre-computed counts)
|
||||
const winRate = returns.length > 0 ? positiveCount / returns.length : 0;
|
||||
const avgWin = positiveCount > 0 ? positiveSum / positiveCount : 0;
|
||||
const avgLoss = negativeCount > 0 ? negativeSum / negativeCount : 0;
|
||||
const profitFactor = negativeSum !== 0 ? positiveSum / Math.abs(negativeSum) : Infinity;
|
||||
const payoffRatio = avgLoss !== 0 ? avgWin / Math.abs(avgLoss) : Infinity;
|
||||
const expectancy = winRate * avgWin - (1 - winRate) * Math.abs(avgLoss);
|
||||
|
||||
// VaR (requires sort - do lazily)
|
||||
const sortedReturns = [...returns].sort((a, b) => a - b);
|
||||
const var95 = -sortedReturns[Math.floor(0.05 * sortedReturns.length)];
|
||||
const var99 = -sortedReturns[Math.floor(0.01 * sortedReturns.length)];
|
||||
|
||||
// CVaR
|
||||
const tailIndex = Math.floor(0.05 * sortedReturns.length);
|
||||
let cvarSum = 0;
|
||||
for (let i = 0; i <= tailIndex; i++) cvarSum += sortedReturns[i];
|
||||
const cvar95 = tailIndex > 0 ? -cvarSum / (tailIndex + 1) : 0;
|
||||
|
||||
// Skewness and Kurtosis (using pre-computed mean/variance)
|
||||
let m3 = 0, m4 = 0;
|
||||
for (let i = 0; i < returns.length; i++) {
|
||||
const d = returns[i] - mean;
|
||||
const d2 = d * d;
|
||||
m3 += d * d2;
|
||||
m4 += d2 * d2;
|
||||
}
|
||||
m3 /= returns.length;
|
||||
m4 /= returns.length;
|
||||
const std = volatility;
|
||||
const skewness = std > 0 ? m3 / (std * std * std) : 0;
|
||||
const kurtosis = std > 0 ? m4 / (std * std * std * std) - 3 : 0;
|
||||
|
||||
// Best/worst day
|
||||
let bestDay = returns[0], worstDay = returns[0];
|
||||
for (let i = 1; i < returns.length; i++) {
|
||||
if (returns[i] > bestDay) bestDay = returns[i];
|
||||
if (returns[i] < worstDay) worstDay = returns[i];
|
||||
}
|
||||
|
||||
// Benchmark metrics
|
||||
let informationRatio = null;
|
||||
if (benchmark) {
|
||||
informationRatio = this.informationRatioFast(returns, benchmark);
|
||||
}
|
||||
|
||||
return {
|
||||
totalReturn: compoundReturn - 1,
|
||||
annualizedReturn,
|
||||
cagr,
|
||||
volatility,
|
||||
annualizedVolatility: annualizedVol,
|
||||
maxDrawdown: ddMetrics.maxDrawdown,
|
||||
averageDrawdown: ddMetrics.averageDrawdown,
|
||||
drawdownDuration: ddMetrics.maxDuration,
|
||||
sharpeRatio: sharpe,
|
||||
sortinoRatio: sortino,
|
||||
calmarRatio: calmar,
|
||||
informationRatio,
|
||||
winRate,
|
||||
profitFactor,
|
||||
averageWin: avgWin,
|
||||
averageLoss: avgLoss,
|
||||
payoffRatio,
|
||||
expectancy,
|
||||
var95,
|
||||
var99,
|
||||
cvar95,
|
||||
skewness,
|
||||
kurtosis,
|
||||
tradingDays: returns.length,
|
||||
bestDay,
|
||||
worstDay,
|
||||
positiveMonths: this.positiveMonthsFast(returns),
|
||||
returns,
|
||||
equityCurve
|
||||
};
|
||||
}
|
||||
|
||||
// Optimized: Single pass drawdown computation
|
||||
computeDrawdownMetrics(equityCurve) {
|
||||
let maxDrawdown = 0;
|
||||
let peak = equityCurve[0];
|
||||
let ddSum = 0;
|
||||
let maxDuration = 0;
|
||||
let currentDuration = 0;
|
||||
|
||||
for (let i = 0; i < equityCurve.length; i++) {
|
||||
const value = equityCurve[i];
|
||||
if (value > peak) {
|
||||
peak = value;
|
||||
currentDuration = 0;
|
||||
} else {
|
||||
currentDuration++;
|
||||
if (currentDuration > maxDuration) maxDuration = currentDuration;
|
||||
}
|
||||
const dd = (peak - value) / peak;
|
||||
ddSum += dd;
|
||||
if (dd > maxDrawdown) maxDrawdown = dd;
|
||||
}
|
||||
|
||||
return {
|
||||
maxDrawdown,
|
||||
averageDrawdown: ddSum / equityCurve.length,
|
||||
maxDuration
|
||||
};
|
||||
}
|
||||
|
||||
// Optimized information ratio
|
||||
informationRatioFast(returns, benchmark) {
|
||||
const benchmarkReturns = this.calculateReturns(benchmark);
|
||||
const minLen = Math.min(returns.length, benchmarkReturns.length);
|
||||
let sum = 0, sumSq = 0;
|
||||
|
||||
for (let i = 0; i < minLen; i++) {
|
||||
const te = returns[i] - benchmarkReturns[i];
|
||||
sum += te;
|
||||
sumSq += te * te;
|
||||
}
|
||||
|
||||
const mean = sum / minLen;
|
||||
const variance = sumSq / minLen - mean * mean;
|
||||
const vol = Math.sqrt(variance);
|
||||
return vol > 0 ? (mean / vol) * Math.sqrt(252) : 0;
|
||||
}
|
||||
|
||||
// Optimized positive months
|
||||
positiveMonthsFast(returns) {
|
||||
let positiveMonths = 0;
|
||||
let totalMonths = 0;
|
||||
let monthReturn = 1;
|
||||
|
||||
for (let i = 0; i < returns.length; i++) {
|
||||
monthReturn *= (1 + returns[i]);
|
||||
if ((i + 1) % 21 === 0 || i === returns.length - 1) {
|
||||
if (monthReturn > 1) positiveMonths++;
|
||||
totalMonths++;
|
||||
monthReturn = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return totalMonths > 0 ? positiveMonths / totalMonths : 0;
|
||||
}
|
||||
|
||||
calculateReturns(equityCurve) {
|
||||
const returns = new Array(equityCurve.length - 1);
|
||||
for (let i = 1; i < equityCurve.length; i++) {
|
||||
returns[i-1] = (equityCurve[i] - equityCurve[i-1]) / equityCurve[i-1];
|
||||
}
|
||||
return returns;
|
||||
}
|
||||
|
||||
emptyMetrics() {
|
||||
return {
|
||||
totalReturn: 0, annualizedReturn: 0, cagr: 0,
|
||||
volatility: 0, annualizedVolatility: 0, maxDrawdown: 0,
|
||||
sharpeRatio: 0, sortinoRatio: 0, calmarRatio: 0,
|
||||
winRate: 0, profitFactor: 0, expectancy: 0,
|
||||
var95: 0, var99: 0, cvar95: 0,
|
||||
tradingDays: 0, returns: [], equityCurve: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backtest Engine
|
||||
*/
|
||||
class BacktestEngine {
|
||||
constructor(config = backtestConfig) {
|
||||
this.config = config;
|
||||
this.metricsCalculator = new PerformanceMetrics(config.riskFreeRate);
|
||||
this.pipeline = createTradingPipeline();
|
||||
}
|
||||
|
||||
// Run backtest on historical data
|
||||
async run(historicalData, options = {}) {
|
||||
const {
|
||||
symbols = ['DEFAULT'],
|
||||
newsData = [],
|
||||
riskManager = null
|
||||
} = options;
|
||||
|
||||
const results = {
|
||||
equityCurve: [this.config.simulation.initialCapital],
|
||||
benchmarkCurve: [this.config.simulation.initialCapital],
|
||||
trades: [],
|
||||
dailyReturns: [],
|
||||
positions: [],
|
||||
signals: []
|
||||
};
|
||||
|
||||
// Initialize portfolio
|
||||
let portfolio = {
|
||||
equity: this.config.simulation.initialCapital,
|
||||
cash: this.config.simulation.initialCapital,
|
||||
positions: {},
|
||||
assets: symbols
|
||||
};
|
||||
|
||||
// Skip warmup period
|
||||
const startIndex = this.config.simulation.warmupPeriod;
|
||||
const prices = {};
|
||||
|
||||
// Process each day
|
||||
for (let i = startIndex; i < historicalData.length; i++) {
|
||||
const dayData = historicalData[i];
|
||||
const currentPrice = dayData.close || dayData.price || 100;
|
||||
|
||||
// Update prices
|
||||
for (const symbol of symbols) {
|
||||
prices[symbol] = currentPrice;
|
||||
}
|
||||
|
||||
// Get historical window for pipeline
|
||||
const windowStart = Math.max(0, i - 100);
|
||||
const marketWindow = historicalData.slice(windowStart, i + 1);
|
||||
|
||||
// Get news for this day (simplified - would filter by date in production)
|
||||
const dayNews = newsData.filter((n, idx) => idx < 3);
|
||||
|
||||
// Execute pipeline
|
||||
const context = {
|
||||
marketData: marketWindow,
|
||||
newsData: dayNews,
|
||||
symbols,
|
||||
portfolio,
|
||||
prices,
|
||||
riskManager
|
||||
};
|
||||
|
||||
try {
|
||||
const pipelineResult = await this.pipeline.execute(context);
|
||||
|
||||
// Store signals
|
||||
if (pipelineResult.signals) {
|
||||
results.signals.push({
|
||||
day: i,
|
||||
signals: pipelineResult.signals
|
||||
});
|
||||
}
|
||||
|
||||
// Execute orders
|
||||
if (pipelineResult.orders && pipelineResult.orders.length > 0) {
|
||||
for (const order of pipelineResult.orders) {
|
||||
const trade = this.executeTrade(order, portfolio, prices);
|
||||
if (trade) {
|
||||
results.trades.push({ day: i, ...trade });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Pipeline error - skip this day
|
||||
console.warn(`Day ${i} pipeline error:`, error.message);
|
||||
}
|
||||
|
||||
// Update portfolio value
|
||||
portfolio.equity = portfolio.cash;
|
||||
for (const [symbol, qty] of Object.entries(portfolio.positions)) {
|
||||
portfolio.equity += qty * (prices[symbol] || 0);
|
||||
}
|
||||
|
||||
results.equityCurve.push(portfolio.equity);
|
||||
results.positions.push({ ...portfolio.positions });
|
||||
|
||||
// Update benchmark (buy and hold)
|
||||
const benchmarkReturn = i > startIndex
|
||||
? (currentPrice / historicalData[i - 1].close) - 1
|
||||
: 0;
|
||||
const lastBenchmark = results.benchmarkCurve[results.benchmarkCurve.length - 1];
|
||||
results.benchmarkCurve.push(lastBenchmark * (1 + benchmarkReturn));
|
||||
|
||||
// Daily return
|
||||
if (results.equityCurve.length >= 2) {
|
||||
const prev = results.equityCurve[results.equityCurve.length - 2];
|
||||
const curr = results.equityCurve[results.equityCurve.length - 1];
|
||||
results.dailyReturns.push((curr - prev) / prev);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate performance metrics
|
||||
results.metrics = this.metricsCalculator.calculate(
|
||||
results.equityCurve,
|
||||
results.benchmarkCurve
|
||||
);
|
||||
|
||||
results.benchmarkMetrics = this.metricsCalculator.calculate(
|
||||
results.benchmarkCurve
|
||||
);
|
||||
|
||||
// Trade statistics
|
||||
results.tradeStats = this.calculateTradeStats(results.trades);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Execute a trade
|
||||
executeTrade(order, portfolio, prices) {
|
||||
const price = prices[order.symbol] || order.price;
|
||||
const value = order.quantity * price;
|
||||
const costs = value * (this.config.execution.slippage + this.config.execution.commission);
|
||||
|
||||
if (order.side === 'buy') {
|
||||
if (portfolio.cash < value + costs) {
|
||||
return null; // Insufficient funds
|
||||
}
|
||||
portfolio.cash -= value + costs;
|
||||
portfolio.positions[order.symbol] = (portfolio.positions[order.symbol] || 0) + order.quantity;
|
||||
} else {
|
||||
const currentQty = portfolio.positions[order.symbol] || 0;
|
||||
if (currentQty < order.quantity) {
|
||||
return null; // Insufficient shares
|
||||
}
|
||||
portfolio.cash += value - costs;
|
||||
portfolio.positions[order.symbol] = currentQty - order.quantity;
|
||||
}
|
||||
|
||||
return {
|
||||
symbol: order.symbol,
|
||||
side: order.side,
|
||||
quantity: order.quantity,
|
||||
price,
|
||||
value,
|
||||
costs,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate trade statistics
|
||||
calculateTradeStats(trades) {
|
||||
if (trades.length === 0) {
|
||||
return { totalTrades: 0, buyTrades: 0, sellTrades: 0, totalVolume: 0, totalCosts: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
totalTrades: trades.length,
|
||||
buyTrades: trades.filter(t => t.side === 'buy').length,
|
||||
sellTrades: trades.filter(t => t.side === 'sell').length,
|
||||
totalVolume: trades.reduce((a, t) => a + t.value, 0),
|
||||
totalCosts: trades.reduce((a, t) => a + t.costs, 0),
|
||||
avgTradeSize: trades.reduce((a, t) => a + t.value, 0) / trades.length
|
||||
};
|
||||
}
|
||||
|
||||
// Generate backtest report
|
||||
generateReport(results) {
|
||||
const m = results.metrics;
|
||||
const b = results.benchmarkMetrics;
|
||||
const t = results.tradeStats;
|
||||
|
||||
return `
|
||||
══════════════════════════════════════════════════════════════════════
|
||||
BACKTEST REPORT
|
||||
══════════════════════════════════════════════════════════════════════
|
||||
|
||||
PERFORMANCE SUMMARY
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Strategy Benchmark Difference
|
||||
Total Return: ${(m.totalReturn * 100).toFixed(2)}% ${(b.totalReturn * 100).toFixed(2)}% ${((m.totalReturn - b.totalReturn) * 100).toFixed(2)}%
|
||||
Annualized Return: ${(m.annualizedReturn * 100).toFixed(2)}% ${(b.annualizedReturn * 100).toFixed(2)}% ${((m.annualizedReturn - b.annualizedReturn) * 100).toFixed(2)}%
|
||||
CAGR: ${(m.cagr * 100).toFixed(2)}% ${(b.cagr * 100).toFixed(2)}% ${((m.cagr - b.cagr) * 100).toFixed(2)}%
|
||||
|
||||
RISK METRICS
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Volatility (Ann.): ${(m.annualizedVolatility * 100).toFixed(2)}% ${(b.annualizedVolatility * 100).toFixed(2)}%
|
||||
Max Drawdown: ${(m.maxDrawdown * 100).toFixed(2)}% ${(b.maxDrawdown * 100).toFixed(2)}%
|
||||
Avg Drawdown: ${(m.averageDrawdown * 100).toFixed(2)}%
|
||||
DD Duration (days): ${m.drawdownDuration}
|
||||
|
||||
RISK-ADJUSTED RETURNS
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Sharpe Ratio: ${m.sharpeRatio.toFixed(2)} ${b.sharpeRatio.toFixed(2)}
|
||||
Sortino Ratio: ${m.sortinoRatio.toFixed(2)} ${b.sortinoRatio.toFixed(2)}
|
||||
Calmar Ratio: ${m.calmarRatio.toFixed(2)} ${b.calmarRatio.toFixed(2)}
|
||||
Information Ratio: ${m.informationRatio?.toFixed(2) || 'N/A'}
|
||||
|
||||
TRADE STATISTICS
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Win Rate: ${(m.winRate * 100).toFixed(1)}%
|
||||
Profit Factor: ${m.profitFactor.toFixed(2)}
|
||||
Avg Win: ${(m.averageWin * 100).toFixed(2)}%
|
||||
Avg Loss: ${(m.averageLoss * 100).toFixed(2)}%
|
||||
Payoff Ratio: ${m.payoffRatio.toFixed(2)}
|
||||
Expectancy: ${(m.expectancy * 100).toFixed(3)}%
|
||||
|
||||
TAIL RISK
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
VaR (95%): ${(m.var95 * 100).toFixed(2)}%
|
||||
VaR (99%): ${(m.var99 * 100).toFixed(2)}%
|
||||
CVaR (95%): ${(m.cvar95 * 100).toFixed(2)}%
|
||||
Skewness: ${m.skewness.toFixed(2)}
|
||||
Kurtosis: ${m.kurtosis.toFixed(2)}
|
||||
|
||||
TRADING ACTIVITY
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Total Trades: ${t.totalTrades}
|
||||
Buy Trades: ${t.buyTrades}
|
||||
Sell Trades: ${t.sellTrades}
|
||||
Total Volume: $${t.totalVolume.toFixed(2)}
|
||||
Total Costs: $${t.totalCosts.toFixed(2)}
|
||||
Avg Trade Size: $${(t.avgTradeSize || 0).toFixed(2)}
|
||||
|
||||
ADDITIONAL METRICS
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Trading Days: ${m.tradingDays}
|
||||
Best Day: ${(m.bestDay * 100).toFixed(2)}%
|
||||
Worst Day: ${(m.worstDay * 100).toFixed(2)}%
|
||||
Positive Months: ${(m.positiveMonths * 100).toFixed(1)}%
|
||||
|
||||
══════════════════════════════════════════════════════════════════════
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk-Forward Analysis
|
||||
*/
|
||||
class WalkForwardAnalyzer {
|
||||
constructor(config = {}) {
|
||||
this.trainRatio = config.trainRatio || 0.7;
|
||||
this.numFolds = config.numFolds || 5;
|
||||
this.engine = new BacktestEngine();
|
||||
}
|
||||
|
||||
async analyze(historicalData, options = {}) {
|
||||
const foldSize = Math.floor(historicalData.length / this.numFolds);
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < this.numFolds; i++) {
|
||||
const testStart = i * foldSize;
|
||||
const testEnd = (i + 1) * foldSize;
|
||||
const trainEnd = Math.floor(testStart * this.trainRatio);
|
||||
|
||||
// In-sample (training) period
|
||||
const trainData = historicalData.slice(0, trainEnd);
|
||||
|
||||
// Out-of-sample (test) period
|
||||
const testData = historicalData.slice(testStart, testEnd);
|
||||
|
||||
// Run backtest on test period
|
||||
const foldResult = await this.engine.run(testData, options);
|
||||
|
||||
results.push({
|
||||
fold: i + 1,
|
||||
trainPeriod: { start: 0, end: trainEnd },
|
||||
testPeriod: { start: testStart, end: testEnd },
|
||||
metrics: foldResult.metrics
|
||||
});
|
||||
}
|
||||
|
||||
// Aggregate results
|
||||
const avgSharpe = results.reduce((a, r) => a + r.metrics.sharpeRatio, 0) / results.length;
|
||||
const avgReturn = results.reduce((a, r) => a + r.metrics.totalReturn, 0) / results.length;
|
||||
|
||||
return {
|
||||
folds: results,
|
||||
aggregate: {
|
||||
avgSharpe,
|
||||
avgReturn,
|
||||
consistency: this.calculateConsistency(results)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
calculateConsistency(results) {
|
||||
const profitableFolds = results.filter(r => r.metrics.totalReturn > 0).length;
|
||||
return profitableFolds / results.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Exports
|
||||
export {
|
||||
BacktestEngine,
|
||||
PerformanceMetrics,
|
||||
WalkForwardAnalyzer,
|
||||
backtestConfig
|
||||
};
|
||||
|
||||
// Demo if run directly
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
console.log('BACKTESTING FRAMEWORK DEMO');
|
||||
console.log('══════════════════════════════════════════════════════════════════════\n');
|
||||
|
||||
// Generate synthetic historical data
|
||||
const generateHistoricalData = (days) => {
|
||||
const data = [];
|
||||
let price = 100;
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
const trend = Math.sin(i / 50) * 0.001; // Cyclical trend
|
||||
const noise = (Math.random() - 0.5) * 0.02; // Random noise
|
||||
const change = trend + noise;
|
||||
|
||||
price *= (1 + change);
|
||||
|
||||
data.push({
|
||||
date: new Date(Date.now() - (days - i) * 24 * 60 * 60 * 1000),
|
||||
open: price * (1 - Math.random() * 0.005),
|
||||
high: price * (1 + Math.random() * 0.01),
|
||||
low: price * (1 - Math.random() * 0.01),
|
||||
close: price,
|
||||
volume: 1000000 * (0.5 + Math.random())
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const historicalData = generateHistoricalData(500);
|
||||
|
||||
console.log('1. Data Summary:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
console.log(` Days: ${historicalData.length}`);
|
||||
console.log(` Start: ${historicalData[0].date.toISOString().split('T')[0]}`);
|
||||
console.log(` End: ${historicalData[historicalData.length-1].date.toISOString().split('T')[0]}`);
|
||||
console.log(` Start Price: $${historicalData[0].close.toFixed(2)}`);
|
||||
console.log(` End Price: $${historicalData[historicalData.length-1].close.toFixed(2)}`);
|
||||
console.log();
|
||||
|
||||
const engine = new BacktestEngine();
|
||||
|
||||
console.log('2. Running Backtest...');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
|
||||
engine.run(historicalData, {
|
||||
symbols: ['TEST'],
|
||||
newsData: [
|
||||
{ symbol: 'TEST', text: 'Strong growth reported in quarterly earnings', source: 'news' },
|
||||
{ symbol: 'TEST', text: 'Analyst upgrades stock to buy rating', source: 'analyst' }
|
||||
]
|
||||
}).then(results => {
|
||||
console.log(engine.generateReport(results));
|
||||
|
||||
console.log('3. Equity Curve Summary:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
console.log(` Initial: $${results.equityCurve[0].toFixed(2)}`);
|
||||
console.log(` Final: $${results.equityCurve[results.equityCurve.length-1].toFixed(2)}`);
|
||||
console.log(` Peak: $${Math.max(...results.equityCurve).toFixed(2)}`);
|
||||
console.log(` Trough: $${Math.min(...results.equityCurve).toFixed(2)}`);
|
||||
|
||||
console.log();
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
console.log('Backtesting demo completed');
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
}).catch(err => {
|
||||
console.error('Backtest error:', err);
|
||||
});
|
||||
}
|
||||
637
vendor/ruvector/examples/neural-trader/system/data-connectors.js
vendored
Normal file
637
vendor/ruvector/examples/neural-trader/system/data-connectors.js
vendored
Normal file
@@ -0,0 +1,637 @@
|
||||
/**
|
||||
* Real Data Connectors
|
||||
*
|
||||
* APIs for market data from multiple sources:
|
||||
* - Yahoo Finance (free, delayed)
|
||||
* - Alpha Vantage (free tier available)
|
||||
* - Binance (crypto, real-time)
|
||||
* - Polygon.io (stocks, options)
|
||||
* - IEX Cloud (stocks)
|
||||
*
|
||||
* Features:
|
||||
* - Rate limiting
|
||||
* - Caching
|
||||
* - Error handling
|
||||
* - Data normalization
|
||||
*/
|
||||
|
||||
// Connector Configuration
|
||||
const connectorConfig = {
|
||||
// API Keys (set via environment or constructor)
|
||||
apiKeys: {
|
||||
alphaVantage: process.env.ALPHA_VANTAGE_KEY || '',
|
||||
polygon: process.env.POLYGON_KEY || '',
|
||||
iex: process.env.IEX_KEY || '',
|
||||
binance: process.env.BINANCE_KEY || ''
|
||||
},
|
||||
|
||||
// Rate limits (requests per minute)
|
||||
rateLimits: {
|
||||
yahoo: 100,
|
||||
alphaVantage: 5,
|
||||
binance: 1200,
|
||||
polygon: 100,
|
||||
iex: 100
|
||||
},
|
||||
|
||||
// Cache settings
|
||||
cache: {
|
||||
enabled: true,
|
||||
ttl: 60000, // 1 minute default
|
||||
maxSize: 1000
|
||||
},
|
||||
|
||||
// Retry settings
|
||||
retry: {
|
||||
maxRetries: 3,
|
||||
backoffMs: 1000
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple LRU Cache
|
||||
*/
|
||||
class LRUCache {
|
||||
constructor(maxSize = 1000, ttl = 60000) {
|
||||
this.maxSize = maxSize;
|
||||
this.ttl = ttl;
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
get(key) {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return null;
|
||||
if (Date.now() - entry.timestamp > this.ttl) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
// Move to end (most recent)
|
||||
this.cache.delete(key);
|
||||
this.cache.set(key, entry);
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
// Remove oldest entry
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
this.cache.set(key, { value, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate Limiter
|
||||
*/
|
||||
class RateLimiter {
|
||||
constructor(requestsPerMinute) {
|
||||
this.requestsPerMinute = requestsPerMinute;
|
||||
this.requests = [];
|
||||
}
|
||||
|
||||
async acquire() {
|
||||
const now = Date.now();
|
||||
// Remove requests older than 1 minute
|
||||
this.requests = this.requests.filter(t => now - t < 60000);
|
||||
|
||||
if (this.requests.length >= this.requestsPerMinute) {
|
||||
const waitTime = 60000 - (now - this.requests[0]);
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
return this.acquire();
|
||||
}
|
||||
|
||||
this.requests.push(now);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base Data Connector
|
||||
*/
|
||||
class BaseConnector {
|
||||
constructor(config = {}) {
|
||||
this.config = { ...connectorConfig, ...config };
|
||||
this.cache = new LRUCache(
|
||||
this.config.cache.maxSize,
|
||||
this.config.cache.ttl
|
||||
);
|
||||
this.rateLimiters = {};
|
||||
}
|
||||
|
||||
getRateLimiter(source) {
|
||||
if (!this.rateLimiters[source]) {
|
||||
this.rateLimiters[source] = new RateLimiter(
|
||||
this.config.rateLimits[source] || 100
|
||||
);
|
||||
}
|
||||
return this.rateLimiters[source];
|
||||
}
|
||||
|
||||
async fetchWithRetry(url, options = {}, source = 'default') {
|
||||
const cacheKey = `${source}:${url}`;
|
||||
|
||||
// Check cache
|
||||
if (this.config.cache.enabled) {
|
||||
const cached = this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
}
|
||||
|
||||
// Rate limit
|
||||
await this.getRateLimiter(source).acquire();
|
||||
|
||||
let lastError;
|
||||
for (let i = 0; i < this.config.retry.maxRetries; i++) {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Cache result
|
||||
if (this.config.cache.enabled) {
|
||||
this.cache.set(cacheKey, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await new Promise(r => setTimeout(r, this.config.retry.backoffMs * (i + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
// Normalize OHLCV data to common format
|
||||
normalizeOHLCV(data, source) {
|
||||
return data.map(d => ({
|
||||
timestamp: new Date(d.timestamp || d.date || d.t).getTime(),
|
||||
open: parseFloat(d.open || d.o || d['1. open'] || 0),
|
||||
high: parseFloat(d.high || d.h || d['2. high'] || 0),
|
||||
low: parseFloat(d.low || d.l || d['3. low'] || 0),
|
||||
close: parseFloat(d.close || d.c || d['4. close'] || 0),
|
||||
volume: parseFloat(d.volume || d.v || d['5. volume'] || 0),
|
||||
source
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Yahoo Finance Connector (via unofficial API)
|
||||
*/
|
||||
class YahooFinanceConnector extends BaseConnector {
|
||||
constructor(config = {}) {
|
||||
super(config);
|
||||
this.baseUrl = 'https://query1.finance.yahoo.com/v8/finance';
|
||||
}
|
||||
|
||||
async getQuote(symbol) {
|
||||
const url = `${this.baseUrl}/chart/${symbol}?interval=1d&range=1d`;
|
||||
const data = await this.fetchWithRetry(url, {}, 'yahoo');
|
||||
|
||||
if (!data.chart?.result?.[0]) {
|
||||
throw new Error(`No data for symbol: ${symbol}`);
|
||||
}
|
||||
|
||||
const result = data.chart.result[0];
|
||||
const quote = result.indicators.quote[0];
|
||||
const meta = result.meta;
|
||||
|
||||
return {
|
||||
symbol: meta.symbol,
|
||||
price: meta.regularMarketPrice,
|
||||
previousClose: meta.previousClose,
|
||||
change: meta.regularMarketPrice - meta.previousClose,
|
||||
changePercent: ((meta.regularMarketPrice - meta.previousClose) / meta.previousClose) * 100,
|
||||
volume: quote.volume?.[quote.volume.length - 1] || 0,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async getHistorical(symbol, period = '1y', interval = '1d') {
|
||||
const url = `${this.baseUrl}/chart/${symbol}?interval=${interval}&range=${period}`;
|
||||
const data = await this.fetchWithRetry(url, {}, 'yahoo');
|
||||
|
||||
if (!data.chart?.result?.[0]) {
|
||||
throw new Error(`No data for symbol: ${symbol}`);
|
||||
}
|
||||
|
||||
const result = data.chart.result[0];
|
||||
const timestamps = result.timestamp;
|
||||
const quote = result.indicators.quote[0];
|
||||
|
||||
const candles = [];
|
||||
for (let i = 0; i < timestamps.length; i++) {
|
||||
if (quote.open[i] !== null) {
|
||||
candles.push({
|
||||
timestamp: timestamps[i] * 1000,
|
||||
open: quote.open[i],
|
||||
high: quote.high[i],
|
||||
low: quote.low[i],
|
||||
close: quote.close[i],
|
||||
volume: quote.volume[i],
|
||||
source: 'yahoo'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return candles;
|
||||
}
|
||||
|
||||
async search(query) {
|
||||
const url = `https://query2.finance.yahoo.com/v1/finance/search?q=${encodeURIComponent(query)}`;
|
||||
const data = await this.fetchWithRetry(url, {}, 'yahoo');
|
||||
return data.quotes?.map(q => ({
|
||||
symbol: q.symbol,
|
||||
name: q.shortname || q.longname,
|
||||
type: q.quoteType,
|
||||
exchange: q.exchange
|
||||
})) || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alpha Vantage Connector
|
||||
*/
|
||||
class AlphaVantageConnector extends BaseConnector {
|
||||
constructor(config = {}) {
|
||||
super(config);
|
||||
this.baseUrl = 'https://www.alphavantage.co/query';
|
||||
this.apiKey = config.apiKey || this.config.apiKeys.alphaVantage;
|
||||
}
|
||||
|
||||
async getQuote(symbol) {
|
||||
if (!this.apiKey) throw new Error('Alpha Vantage API key required');
|
||||
|
||||
const url = `${this.baseUrl}?function=GLOBAL_QUOTE&symbol=${symbol}&apikey=${this.apiKey}`;
|
||||
const data = await this.fetchWithRetry(url, {}, 'alphaVantage');
|
||||
|
||||
const quote = data['Global Quote'];
|
||||
if (!quote) throw new Error(`No data for symbol: ${symbol}`);
|
||||
|
||||
return {
|
||||
symbol: quote['01. symbol'],
|
||||
price: parseFloat(quote['05. price']),
|
||||
previousClose: parseFloat(quote['08. previous close']),
|
||||
change: parseFloat(quote['09. change']),
|
||||
changePercent: parseFloat(quote['10. change percent'].replace('%', '')),
|
||||
volume: parseInt(quote['06. volume']),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async getHistorical(symbol, outputSize = 'compact') {
|
||||
if (!this.apiKey) throw new Error('Alpha Vantage API key required');
|
||||
|
||||
const url = `${this.baseUrl}?function=TIME_SERIES_DAILY&symbol=${symbol}&outputsize=${outputSize}&apikey=${this.apiKey}`;
|
||||
const data = await this.fetchWithRetry(url, {}, 'alphaVantage');
|
||||
|
||||
const timeSeries = data['Time Series (Daily)'];
|
||||
if (!timeSeries) throw new Error(`No data for symbol: ${symbol}`);
|
||||
|
||||
return Object.entries(timeSeries).map(([date, values]) => ({
|
||||
timestamp: new Date(date).getTime(),
|
||||
open: parseFloat(values['1. open']),
|
||||
high: parseFloat(values['2. high']),
|
||||
low: parseFloat(values['3. low']),
|
||||
close: parseFloat(values['4. close']),
|
||||
volume: parseInt(values['5. volume']),
|
||||
source: 'alphaVantage'
|
||||
})).sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
async getIntraday(symbol, interval = '5min') {
|
||||
if (!this.apiKey) throw new Error('Alpha Vantage API key required');
|
||||
|
||||
const url = `${this.baseUrl}?function=TIME_SERIES_INTRADAY&symbol=${symbol}&interval=${interval}&apikey=${this.apiKey}`;
|
||||
const data = await this.fetchWithRetry(url, {}, 'alphaVantage');
|
||||
|
||||
const key = `Time Series (${interval})`;
|
||||
const timeSeries = data[key];
|
||||
if (!timeSeries) throw new Error(`No data for symbol: ${symbol}`);
|
||||
|
||||
return Object.entries(timeSeries).map(([datetime, values]) => ({
|
||||
timestamp: new Date(datetime).getTime(),
|
||||
open: parseFloat(values['1. open']),
|
||||
high: parseFloat(values['2. high']),
|
||||
low: parseFloat(values['3. low']),
|
||||
close: parseFloat(values['4. close']),
|
||||
volume: parseInt(values['5. volume']),
|
||||
source: 'alphaVantage'
|
||||
})).sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
async getSentiment(tickers) {
|
||||
if (!this.apiKey) throw new Error('Alpha Vantage API key required');
|
||||
|
||||
const tickerList = Array.isArray(tickers) ? tickers.join(',') : tickers;
|
||||
const url = `${this.baseUrl}?function=NEWS_SENTIMENT&tickers=${tickerList}&apikey=${this.apiKey}`;
|
||||
const data = await this.fetchWithRetry(url, {}, 'alphaVantage');
|
||||
|
||||
return data.feed?.map(item => ({
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
source: item.source,
|
||||
summary: item.summary,
|
||||
sentiment: item.overall_sentiment_score,
|
||||
sentimentLabel: item.overall_sentiment_label,
|
||||
tickers: item.ticker_sentiment,
|
||||
timestamp: new Date(item.time_published).getTime()
|
||||
})) || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Binance Connector (Crypto)
|
||||
*/
|
||||
class BinanceConnector extends BaseConnector {
|
||||
constructor(config = {}) {
|
||||
super(config);
|
||||
this.baseUrl = 'https://api.binance.com/api/v3';
|
||||
this.wsUrl = 'wss://stream.binance.com:9443/ws';
|
||||
}
|
||||
|
||||
async getQuote(symbol) {
|
||||
const url = `${this.baseUrl}/ticker/24hr?symbol=${symbol}`;
|
||||
const data = await this.fetchWithRetry(url, {}, 'binance');
|
||||
|
||||
return {
|
||||
symbol: data.symbol,
|
||||
price: parseFloat(data.lastPrice),
|
||||
previousClose: parseFloat(data.prevClosePrice),
|
||||
change: parseFloat(data.priceChange),
|
||||
changePercent: parseFloat(data.priceChangePercent),
|
||||
volume: parseFloat(data.volume),
|
||||
quoteVolume: parseFloat(data.quoteVolume),
|
||||
high24h: parseFloat(data.highPrice),
|
||||
low24h: parseFloat(data.lowPrice),
|
||||
timestamp: data.closeTime
|
||||
};
|
||||
}
|
||||
|
||||
async getHistorical(symbol, interval = '1d', limit = 500) {
|
||||
const url = `${this.baseUrl}/klines?symbol=${symbol}&interval=${interval}&limit=${limit}`;
|
||||
const data = await this.fetchWithRetry(url, {}, 'binance');
|
||||
|
||||
return data.map(candle => ({
|
||||
timestamp: candle[0],
|
||||
open: parseFloat(candle[1]),
|
||||
high: parseFloat(candle[2]),
|
||||
low: parseFloat(candle[3]),
|
||||
close: parseFloat(candle[4]),
|
||||
volume: parseFloat(candle[5]),
|
||||
closeTime: candle[6],
|
||||
quoteVolume: parseFloat(candle[7]),
|
||||
trades: candle[8],
|
||||
source: 'binance'
|
||||
}));
|
||||
}
|
||||
|
||||
async getOrderBook(symbol, limit = 100) {
|
||||
const url = `${this.baseUrl}/depth?symbol=${symbol}&limit=${limit}`;
|
||||
const data = await this.fetchWithRetry(url, {}, 'binance');
|
||||
|
||||
return {
|
||||
lastUpdateId: data.lastUpdateId,
|
||||
bids: data.bids.map(([price, qty]) => ({
|
||||
price: parseFloat(price),
|
||||
quantity: parseFloat(qty)
|
||||
})),
|
||||
asks: data.asks.map(([price, qty]) => ({
|
||||
price: parseFloat(price),
|
||||
quantity: parseFloat(qty)
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
async getTrades(symbol, limit = 100) {
|
||||
const url = `${this.baseUrl}/trades?symbol=${symbol}&limit=${limit}`;
|
||||
const data = await this.fetchWithRetry(url, {}, 'binance');
|
||||
|
||||
return data.map(trade => ({
|
||||
id: trade.id,
|
||||
price: parseFloat(trade.price),
|
||||
quantity: parseFloat(trade.qty),
|
||||
time: trade.time,
|
||||
isBuyerMaker: trade.isBuyerMaker
|
||||
}));
|
||||
}
|
||||
|
||||
// WebSocket subscription for real-time data
|
||||
subscribeToTrades(symbol, callback) {
|
||||
const ws = new WebSocket(`${this.wsUrl}/${symbol.toLowerCase()}@trade`);
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
callback({
|
||||
symbol: data.s,
|
||||
price: parseFloat(data.p),
|
||||
quantity: parseFloat(data.q),
|
||||
time: data.T,
|
||||
isBuyerMaker: data.m
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
close: () => ws.close()
|
||||
};
|
||||
}
|
||||
|
||||
subscribeToKlines(symbol, interval, callback) {
|
||||
const ws = new WebSocket(`${this.wsUrl}/${symbol.toLowerCase()}@kline_${interval}`);
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
const k = data.k;
|
||||
callback({
|
||||
symbol: k.s,
|
||||
interval: k.i,
|
||||
open: parseFloat(k.o),
|
||||
high: parseFloat(k.h),
|
||||
low: parseFloat(k.l),
|
||||
close: parseFloat(k.c),
|
||||
volume: parseFloat(k.v),
|
||||
isClosed: k.x,
|
||||
timestamp: k.t
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
close: () => ws.close()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified Data Manager
|
||||
*/
|
||||
class DataManager {
|
||||
constructor(config = {}) {
|
||||
this.config = { ...connectorConfig, ...config };
|
||||
this.connectors = {
|
||||
yahoo: new YahooFinanceConnector(config),
|
||||
alphaVantage: new AlphaVantageConnector(config),
|
||||
binance: new BinanceConnector(config)
|
||||
};
|
||||
this.preferredSource = config.preferredSource || 'yahoo';
|
||||
}
|
||||
|
||||
// Get connector by name
|
||||
getConnector(name) {
|
||||
return this.connectors[name];
|
||||
}
|
||||
|
||||
// Smart quote - try preferred source, fallback to others
|
||||
async getQuote(symbol, source = null) {
|
||||
const sources = source ? [source] : [this.preferredSource, 'yahoo', 'alphaVantage'];
|
||||
|
||||
for (const src of sources) {
|
||||
try {
|
||||
const connector = this.connectors[src];
|
||||
if (connector) {
|
||||
return await connector.getQuote(symbol);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Quote failed for ${symbol} from ${src}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to get quote for ${symbol} from all sources`);
|
||||
}
|
||||
|
||||
// Get historical data with source selection
|
||||
async getHistorical(symbol, options = {}) {
|
||||
const {
|
||||
source = this.preferredSource,
|
||||
period = '1y',
|
||||
interval = '1d'
|
||||
} = options;
|
||||
|
||||
const connector = this.connectors[source];
|
||||
if (!connector) throw new Error(`Unknown source: ${source}`);
|
||||
|
||||
if (source === 'yahoo') {
|
||||
return connector.getHistorical(symbol, period, interval);
|
||||
} else if (source === 'alphaVantage') {
|
||||
return connector.getHistorical(symbol, period === '1y' ? 'full' : 'compact');
|
||||
} else if (source === 'binance') {
|
||||
return connector.getHistorical(symbol, interval);
|
||||
}
|
||||
}
|
||||
|
||||
// Get multiple symbols in parallel
|
||||
async getQuotes(symbols) {
|
||||
const promises = symbols.map(s => this.getQuote(s).catch(e => ({ symbol: s, error: e.message })));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
// Get news sentiment
|
||||
async getSentiment(symbols, source = 'alphaVantage') {
|
||||
const connector = this.connectors[source];
|
||||
if (connector?.getSentiment) {
|
||||
return connector.getSentiment(symbols);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Clear all caches
|
||||
clearCache() {
|
||||
for (const connector of Object.values(this.connectors)) {
|
||||
connector.cache?.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exports
|
||||
export {
|
||||
DataManager,
|
||||
YahooFinanceConnector,
|
||||
AlphaVantageConnector,
|
||||
BinanceConnector,
|
||||
BaseConnector,
|
||||
LRUCache,
|
||||
RateLimiter,
|
||||
connectorConfig
|
||||
};
|
||||
|
||||
// Demo if run directly
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
console.log('DATA CONNECTORS DEMO');
|
||||
console.log('══════════════════════════════════════════════════════════════════════\n');
|
||||
|
||||
console.log('Available Connectors:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
console.log(' • Yahoo Finance - Free, delayed quotes, historical data');
|
||||
console.log(' • Alpha Vantage - Free tier (5 req/min), sentiment analysis');
|
||||
console.log(' • Binance - Real-time crypto, WebSocket support');
|
||||
console.log();
|
||||
|
||||
console.log('Features:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
console.log(' • Rate limiting per source');
|
||||
console.log(' • LRU caching with TTL');
|
||||
console.log(' • Automatic retry with backoff');
|
||||
console.log(' • Data normalization to OHLCV format');
|
||||
console.log(' • Multi-source fallback');
|
||||
console.log();
|
||||
|
||||
console.log('Example Usage:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
console.log(`
|
||||
import { DataManager } from './data-connectors.js';
|
||||
|
||||
const data = new DataManager({
|
||||
apiKeys: { alphaVantage: 'YOUR_KEY' }
|
||||
});
|
||||
|
||||
// Get quote
|
||||
const quote = await data.getQuote('AAPL');
|
||||
|
||||
// Get historical data
|
||||
const history = await data.getHistorical('AAPL', { period: '1y' });
|
||||
|
||||
// Get crypto data
|
||||
const btc = await data.getQuote('BTCUSDT', 'binance');
|
||||
const klines = await data.getHistorical('BTCUSDT', {
|
||||
source: 'binance',
|
||||
interval: '1h'
|
||||
});
|
||||
|
||||
// Get sentiment
|
||||
const sentiment = await data.getSentiment(['AAPL', 'MSFT']);
|
||||
`);
|
||||
|
||||
// Test with mock data (no actual API calls)
|
||||
console.log('\nSimulated Output:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
|
||||
const mockQuote = {
|
||||
symbol: 'AAPL',
|
||||
price: 178.50,
|
||||
previousClose: 177.25,
|
||||
change: 1.25,
|
||||
changePercent: 0.71,
|
||||
volume: 52847300,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
console.log('Quote (AAPL):');
|
||||
console.log(` Price: $${mockQuote.price}`);
|
||||
console.log(` Change: $${mockQuote.change} (${mockQuote.changePercent.toFixed(2)}%)`);
|
||||
console.log(` Volume: ${mockQuote.volume.toLocaleString()}`);
|
||||
|
||||
console.log();
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
console.log('Data connectors ready for integration');
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
}
|
||||
883
vendor/ruvector/examples/neural-trader/system/risk-management.js
vendored
Normal file
883
vendor/ruvector/examples/neural-trader/system/risk-management.js
vendored
Normal file
@@ -0,0 +1,883 @@
|
||||
/**
|
||||
* Risk Management Layer
|
||||
*
|
||||
* Comprehensive risk controls for trading systems:
|
||||
* - Position limits (per-asset and portfolio)
|
||||
* - Stop-loss orders (fixed, trailing, volatility-based)
|
||||
* - Circuit breakers (drawdown, loss rate, volatility)
|
||||
* - Exposure management
|
||||
* - Correlation risk
|
||||
* - Leverage control
|
||||
*/
|
||||
|
||||
// Risk Management Configuration
|
||||
const riskConfig = {
|
||||
// Position limits
|
||||
positions: {
|
||||
maxPositionSize: 0.10, // Max 10% per position
|
||||
maxPositionValue: 50000, // Max $50k per position
|
||||
minPositionSize: 0.01, // Min 1% position
|
||||
maxOpenPositions: 20, // Max concurrent positions
|
||||
maxSectorExposure: 0.30, // Max 30% per sector
|
||||
maxCorrelatedExposure: 0.40 // Max 40% in correlated assets
|
||||
},
|
||||
|
||||
// Portfolio limits
|
||||
portfolio: {
|
||||
maxLongExposure: 1.0, // Max 100% long
|
||||
maxShortExposure: 0.5, // Max 50% short
|
||||
maxGrossExposure: 1.5, // Max 150% gross
|
||||
maxNetExposure: 1.0, // Max 100% net
|
||||
maxLeverage: 2.0, // Max 2x leverage
|
||||
minCashReserve: 0.05 // Keep 5% cash
|
||||
},
|
||||
|
||||
// Stop-loss settings
|
||||
stopLoss: {
|
||||
defaultType: 'trailing', // fixed, trailing, volatility
|
||||
fixedPercent: 0.05, // 5% fixed stop
|
||||
trailingPercent: 0.03, // 3% trailing stop
|
||||
volatilityMultiplier: 2.0, // 2x ATR for vol stop
|
||||
maxLossPerTrade: 0.02, // Max 2% loss per trade
|
||||
maxDailyLoss: 0.05 // Max 5% daily loss
|
||||
},
|
||||
|
||||
// Circuit breakers
|
||||
circuitBreakers: {
|
||||
drawdownThreshold: 0.10, // 10% drawdown triggers
|
||||
drawdownCooldown: 86400000, // 24h cooldown
|
||||
lossRateThreshold: 0.70, // 70% loss rate in window
|
||||
lossRateWindow: 20, // 20 trade window
|
||||
volatilityThreshold: 0.04, // 4% daily vol threshold
|
||||
volatilityMultiplier: 3.0, // 3x normal vol
|
||||
consecutiveLosses: 5 // 5 consecutive losses
|
||||
},
|
||||
|
||||
// Risk scoring
|
||||
scoring: {
|
||||
updateFrequency: 60000, // Update every minute
|
||||
historyWindow: 252, // 1 year of daily data
|
||||
correlationThreshold: 0.7 // High correlation threshold
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop-Loss Manager
|
||||
*/
|
||||
class StopLossManager {
|
||||
constructor(config = riskConfig.stopLoss) {
|
||||
this.config = config;
|
||||
this.stops = new Map(); // symbol -> stop config
|
||||
this.volatility = new Map(); // symbol -> ATR
|
||||
}
|
||||
|
||||
// Set stop-loss for a position
|
||||
setStop(symbol, entryPrice, type = null, params = {}) {
|
||||
const stopType = type || this.config.defaultType;
|
||||
let stopPrice;
|
||||
|
||||
switch (stopType) {
|
||||
case 'fixed':
|
||||
stopPrice = entryPrice * (1 - this.config.fixedPercent);
|
||||
break;
|
||||
|
||||
case 'trailing':
|
||||
stopPrice = entryPrice * (1 - this.config.trailingPercent);
|
||||
break;
|
||||
|
||||
case 'volatility':
|
||||
const atr = this.volatility.get(symbol) || entryPrice * 0.02;
|
||||
stopPrice = entryPrice - (atr * this.config.volatilityMultiplier);
|
||||
break;
|
||||
|
||||
default:
|
||||
stopPrice = entryPrice * (1 - this.config.fixedPercent);
|
||||
}
|
||||
|
||||
this.stops.set(symbol, {
|
||||
type: stopType,
|
||||
entryPrice,
|
||||
stopPrice,
|
||||
highWaterMark: entryPrice,
|
||||
params,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
|
||||
return this.stops.get(symbol);
|
||||
}
|
||||
|
||||
// Update trailing stop with new price
|
||||
updateTrailingStop(symbol, currentPrice) {
|
||||
const stop = this.stops.get(symbol);
|
||||
if (!stop || stop.type !== 'trailing') return null;
|
||||
|
||||
if (currentPrice > stop.highWaterMark) {
|
||||
stop.highWaterMark = currentPrice;
|
||||
stop.stopPrice = currentPrice * (1 - this.config.trailingPercent);
|
||||
}
|
||||
|
||||
return stop;
|
||||
}
|
||||
|
||||
// Check if stop is triggered
|
||||
checkStop(symbol, currentPrice) {
|
||||
const stop = this.stops.get(symbol);
|
||||
if (!stop) return { triggered: false };
|
||||
|
||||
// Update trailing stop first
|
||||
if (stop.type === 'trailing') {
|
||||
this.updateTrailingStop(symbol, currentPrice);
|
||||
}
|
||||
|
||||
const triggered = currentPrice <= stop.stopPrice;
|
||||
|
||||
return {
|
||||
triggered,
|
||||
stopPrice: stop.stopPrice,
|
||||
currentPrice,
|
||||
loss: triggered ? (stop.entryPrice - currentPrice) / stop.entryPrice : 0,
|
||||
type: stop.type
|
||||
};
|
||||
}
|
||||
|
||||
// Set volatility for volatility-based stops
|
||||
setVolatility(symbol, atr) {
|
||||
this.volatility.set(symbol, atr);
|
||||
}
|
||||
|
||||
// Remove stop
|
||||
removeStop(symbol) {
|
||||
this.stops.delete(symbol);
|
||||
}
|
||||
|
||||
// Get all active stops
|
||||
getActiveStops() {
|
||||
return Object.fromEntries(this.stops);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Circuit Breaker System
|
||||
*/
|
||||
class CircuitBreaker {
|
||||
constructor(config = riskConfig.circuitBreakers) {
|
||||
this.config = config;
|
||||
this.state = {
|
||||
isTripped: false,
|
||||
tripReason: null,
|
||||
tripTime: null,
|
||||
cooldownUntil: null
|
||||
};
|
||||
|
||||
// Tracking data
|
||||
this.peakEquity = 0;
|
||||
this.currentEquity = 0;
|
||||
this.consecutiveLosses = 0;
|
||||
|
||||
// Optimized: Use ring buffers instead of arrays with shift/slice
|
||||
const tradeWindowSize = config.lossRateWindow * 2;
|
||||
this._tradeBuffer = new Array(tradeWindowSize);
|
||||
this._tradeIndex = 0;
|
||||
this._tradeCount = 0;
|
||||
this._tradeLossCount = 0; // Track losses incrementally
|
||||
|
||||
this._volBuffer = new Array(20);
|
||||
this._volIndex = 0;
|
||||
this._volCount = 0;
|
||||
this._volSum = 0; // Running sum for O(1) average
|
||||
}
|
||||
|
||||
// Update with new equity value
|
||||
updateEquity(equity) {
|
||||
this.currentEquity = equity;
|
||||
if (equity > this.peakEquity) {
|
||||
this.peakEquity = equity;
|
||||
}
|
||||
|
||||
// Check drawdown breaker
|
||||
const drawdown = (this.peakEquity - equity) / this.peakEquity;
|
||||
if (drawdown >= this.config.drawdownThreshold) {
|
||||
this.trip('drawdown', `Drawdown ${(drawdown * 100).toFixed(1)}% exceeds threshold`);
|
||||
}
|
||||
}
|
||||
|
||||
// Optimized: Record trade with O(1) ring buffer
|
||||
recordTrade(profit) {
|
||||
const bufferSize = this._tradeBuffer.length;
|
||||
const windowSize = this.config.lossRateWindow;
|
||||
|
||||
// If overwriting an old trade, adjust loss count
|
||||
if (this._tradeCount >= bufferSize) {
|
||||
const oldTrade = this._tradeBuffer[this._tradeIndex];
|
||||
if (oldTrade && oldTrade.profit < 0) {
|
||||
this._tradeLossCount--;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new trade
|
||||
this._tradeBuffer[this._tradeIndex] = { profit, timestamp: Date.now() };
|
||||
if (profit < 0) this._tradeLossCount++;
|
||||
|
||||
this._tradeIndex = (this._tradeIndex + 1) % bufferSize;
|
||||
if (this._tradeCount < bufferSize) this._tradeCount++;
|
||||
|
||||
// Update consecutive losses
|
||||
if (profit < 0) {
|
||||
this.consecutiveLosses++;
|
||||
} else {
|
||||
this.consecutiveLosses = 0;
|
||||
}
|
||||
|
||||
// Check loss rate breaker (O(1) using tracked count)
|
||||
if (this._tradeCount >= windowSize) {
|
||||
// Count losses in recent window
|
||||
let recentLosses = 0;
|
||||
const startIdx = (this._tradeIndex - windowSize + bufferSize) % bufferSize;
|
||||
for (let i = 0; i < windowSize; i++) {
|
||||
const idx = (startIdx + i) % bufferSize;
|
||||
if (this._tradeBuffer[idx] && this._tradeBuffer[idx].profit < 0) {
|
||||
recentLosses++;
|
||||
}
|
||||
}
|
||||
const lossRate = recentLosses / windowSize;
|
||||
|
||||
if (lossRate >= this.config.lossRateThreshold) {
|
||||
this.trip('lossRate', `Loss rate ${(lossRate * 100).toFixed(1)}% exceeds threshold`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check consecutive losses breaker
|
||||
if (this.consecutiveLosses >= this.config.consecutiveLosses) {
|
||||
this.trip('consecutiveLosses', `${this.consecutiveLosses} consecutive losses`);
|
||||
}
|
||||
}
|
||||
|
||||
// Optimized: Update volatility with O(1) ring buffer and running sum
|
||||
updateVolatility(dailyReturn) {
|
||||
const absReturn = Math.abs(dailyReturn);
|
||||
const bufferSize = this._volBuffer.length;
|
||||
|
||||
// If overwriting old value, subtract from running sum
|
||||
if (this._volCount >= bufferSize) {
|
||||
this._volSum -= this._volBuffer[this._volIndex];
|
||||
}
|
||||
|
||||
// Add new value
|
||||
this._volBuffer[this._volIndex] = absReturn;
|
||||
this._volSum += absReturn;
|
||||
|
||||
this._volIndex = (this._volIndex + 1) % bufferSize;
|
||||
if (this._volCount < bufferSize) this._volCount++;
|
||||
|
||||
// Check volatility spike (O(1) using running sum)
|
||||
if (this._volCount >= 5) {
|
||||
const avgVol = (this._volSum - absReturn) / (this._volCount - 1);
|
||||
const currentVol = absReturn;
|
||||
|
||||
if (currentVol > avgVol * this.config.volatilityMultiplier ||
|
||||
currentVol > this.config.volatilityThreshold) {
|
||||
this.trip('volatility', `Volatility spike: ${(currentVol * 100).toFixed(2)}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trip the circuit breaker
|
||||
trip(reason, message) {
|
||||
if (this.state.isTripped) return; // Already tripped
|
||||
|
||||
this.state = {
|
||||
isTripped: true,
|
||||
tripReason: reason,
|
||||
tripMessage: message,
|
||||
tripTime: Date.now(),
|
||||
cooldownUntil: Date.now() + this.config.drawdownCooldown
|
||||
};
|
||||
|
||||
console.warn(`🔴 CIRCUIT BREAKER TRIPPED: ${message}`);
|
||||
}
|
||||
|
||||
// Check if trading is allowed
|
||||
canTrade() {
|
||||
if (!this.state.isTripped) return { allowed: true };
|
||||
|
||||
// Check if cooldown has passed
|
||||
if (Date.now() >= this.state.cooldownUntil) {
|
||||
this.reset();
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
reason: this.state.tripReason,
|
||||
message: this.state.tripMessage,
|
||||
cooldownRemaining: this.state.cooldownUntil - Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// Reset circuit breaker
|
||||
reset() {
|
||||
this.state = {
|
||||
isTripped: false,
|
||||
tripReason: null,
|
||||
tripTime: null,
|
||||
cooldownUntil: null
|
||||
};
|
||||
this.consecutiveLosses = 0;
|
||||
console.log('🟢 Circuit breaker reset');
|
||||
}
|
||||
|
||||
// Force reset (manual override)
|
||||
forceReset() {
|
||||
this.reset();
|
||||
this.peakEquity = this.currentEquity;
|
||||
// Reset ring buffers
|
||||
this._tradeIndex = 0;
|
||||
this._tradeCount = 0;
|
||||
this._tradeLossCount = 0;
|
||||
this._volIndex = 0;
|
||||
this._volCount = 0;
|
||||
this._volSum = 0;
|
||||
}
|
||||
|
||||
getState() {
|
||||
return {
|
||||
...this.state,
|
||||
drawdown: this.peakEquity > 0 ? (this.peakEquity - this.currentEquity) / this.peakEquity : 0,
|
||||
consecutiveLosses: this.consecutiveLosses,
|
||||
recentLossRate: this.calculateRecentLossRate()
|
||||
};
|
||||
}
|
||||
|
||||
// Optimized: O(windowSize) but only called for reporting
|
||||
calculateRecentLossRate() {
|
||||
const windowSize = this.config.lossRateWindow;
|
||||
const count = Math.min(this._tradeCount, windowSize);
|
||||
if (count === 0) return 0;
|
||||
|
||||
let losses = 0;
|
||||
const bufferSize = this._tradeBuffer.length;
|
||||
const startIdx = (this._tradeIndex - count + bufferSize) % bufferSize;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const idx = (startIdx + i) % bufferSize;
|
||||
if (this._tradeBuffer[idx] && this._tradeBuffer[idx].profit < 0) {
|
||||
losses++;
|
||||
}
|
||||
}
|
||||
|
||||
return losses / count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Position Limit Manager
|
||||
*/
|
||||
class PositionLimitManager {
|
||||
constructor(config = riskConfig.positions) {
|
||||
this.config = config;
|
||||
this.positions = new Map();
|
||||
this.sectors = new Map(); // symbol -> sector mapping
|
||||
}
|
||||
|
||||
// Set sector for a symbol
|
||||
setSector(symbol, sector) {
|
||||
this.sectors.set(symbol, sector);
|
||||
}
|
||||
|
||||
// Check if position size is allowed
|
||||
checkPositionSize(symbol, proposedSize, portfolioValue) {
|
||||
const sizePercent = proposedSize / portfolioValue;
|
||||
const violations = [];
|
||||
|
||||
// Check max position size
|
||||
if (sizePercent > this.config.maxPositionSize) {
|
||||
violations.push({
|
||||
type: 'maxPositionSize',
|
||||
message: `Position ${(sizePercent * 100).toFixed(1)}% exceeds max ${(this.config.maxPositionSize * 100)}%`,
|
||||
limit: this.config.maxPositionSize * portfolioValue
|
||||
});
|
||||
}
|
||||
|
||||
// Check max position value
|
||||
if (proposedSize > this.config.maxPositionValue) {
|
||||
violations.push({
|
||||
type: 'maxPositionValue',
|
||||
message: `Position $${proposedSize.toFixed(0)} exceeds max $${this.config.maxPositionValue}`,
|
||||
limit: this.config.maxPositionValue
|
||||
});
|
||||
}
|
||||
|
||||
// Check min position size
|
||||
if (sizePercent < this.config.minPositionSize && proposedSize > 0) {
|
||||
violations.push({
|
||||
type: 'minPositionSize',
|
||||
message: `Position ${(sizePercent * 100).toFixed(1)}% below min ${(this.config.minPositionSize * 100)}%`,
|
||||
limit: this.config.minPositionSize * portfolioValue
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: violations.length === 0,
|
||||
violations,
|
||||
adjustedSize: this.adjustPositionSize(proposedSize, portfolioValue)
|
||||
};
|
||||
}
|
||||
|
||||
// Adjust position size to comply with limits
|
||||
adjustPositionSize(proposedSize, portfolioValue) {
|
||||
let adjusted = proposedSize;
|
||||
|
||||
// Apply max position size
|
||||
const maxByPercent = portfolioValue * this.config.maxPositionSize;
|
||||
adjusted = Math.min(adjusted, maxByPercent);
|
||||
|
||||
// Apply max position value
|
||||
adjusted = Math.min(adjusted, this.config.maxPositionValue);
|
||||
|
||||
return adjusted;
|
||||
}
|
||||
|
||||
// Check sector exposure
|
||||
checkSectorExposure(symbol, proposedSize, currentPositions, portfolioValue) {
|
||||
const sector = this.sectors.get(symbol);
|
||||
if (!sector) return { allowed: true };
|
||||
|
||||
// Calculate current sector exposure
|
||||
let sectorExposure = 0;
|
||||
for (const [sym, pos] of Object.entries(currentPositions)) {
|
||||
if (this.sectors.get(sym) === sector) {
|
||||
sectorExposure += Math.abs(pos.value || 0);
|
||||
}
|
||||
}
|
||||
|
||||
const totalSectorExposure = (sectorExposure + proposedSize) / portfolioValue;
|
||||
|
||||
if (totalSectorExposure > this.config.maxSectorExposure) {
|
||||
return {
|
||||
allowed: false,
|
||||
message: `Sector ${sector} exposure ${(totalSectorExposure * 100).toFixed(1)}% exceeds max ${(this.config.maxSectorExposure * 100)}%`,
|
||||
currentExposure: sectorExposure,
|
||||
maxAllowed: this.config.maxSectorExposure * portfolioValue - sectorExposure
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true, sectorExposure: totalSectorExposure };
|
||||
}
|
||||
|
||||
// Check number of open positions
|
||||
checkPositionCount(currentPositions) {
|
||||
const count = Object.keys(currentPositions).filter(s => currentPositions[s].quantity !== 0).length;
|
||||
|
||||
if (count >= this.config.maxOpenPositions) {
|
||||
return {
|
||||
allowed: false,
|
||||
message: `Max open positions (${this.config.maxOpenPositions}) reached`,
|
||||
currentCount: count
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true, currentCount: count };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposure Manager
|
||||
*/
|
||||
class ExposureManager {
|
||||
constructor(config = riskConfig.portfolio) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
// Calculate portfolio exposure
|
||||
calculateExposure(positions, portfolioValue) {
|
||||
let longExposure = 0;
|
||||
let shortExposure = 0;
|
||||
|
||||
for (const pos of Object.values(positions)) {
|
||||
const value = pos.value || (pos.quantity * pos.price) || 0;
|
||||
if (value > 0) {
|
||||
longExposure += value;
|
||||
} else {
|
||||
shortExposure += Math.abs(value);
|
||||
}
|
||||
}
|
||||
|
||||
const grossExposure = longExposure + shortExposure;
|
||||
const netExposure = longExposure - shortExposure;
|
||||
|
||||
return {
|
||||
long: longExposure / portfolioValue,
|
||||
short: shortExposure / portfolioValue,
|
||||
gross: grossExposure / portfolioValue,
|
||||
net: netExposure / portfolioValue,
|
||||
leverage: grossExposure / portfolioValue,
|
||||
longValue: longExposure,
|
||||
shortValue: shortExposure
|
||||
};
|
||||
}
|
||||
|
||||
// Check if trade would violate exposure limits
|
||||
checkExposure(proposedTrade, currentPositions, portfolioValue) {
|
||||
// Simulate new exposure
|
||||
const newPositions = { ...currentPositions };
|
||||
const symbol = proposedTrade.symbol;
|
||||
const value = proposedTrade.value || (proposedTrade.quantity * proposedTrade.price);
|
||||
const side = proposedTrade.side;
|
||||
|
||||
newPositions[symbol] = {
|
||||
...newPositions[symbol],
|
||||
value: (newPositions[symbol]?.value || 0) + (side === 'buy' ? value : -value)
|
||||
};
|
||||
|
||||
const exposure = this.calculateExposure(newPositions, portfolioValue);
|
||||
const violations = [];
|
||||
|
||||
if (exposure.long > this.config.maxLongExposure) {
|
||||
violations.push({
|
||||
type: 'maxLongExposure',
|
||||
message: `Long exposure ${(exposure.long * 100).toFixed(1)}% exceeds max ${(this.config.maxLongExposure * 100)}%`
|
||||
});
|
||||
}
|
||||
|
||||
if (exposure.short > this.config.maxShortExposure) {
|
||||
violations.push({
|
||||
type: 'maxShortExposure',
|
||||
message: `Short exposure ${(exposure.short * 100).toFixed(1)}% exceeds max ${(this.config.maxShortExposure * 100)}%`
|
||||
});
|
||||
}
|
||||
|
||||
if (exposure.gross > this.config.maxGrossExposure) {
|
||||
violations.push({
|
||||
type: 'maxGrossExposure',
|
||||
message: `Gross exposure ${(exposure.gross * 100).toFixed(1)}% exceeds max ${(this.config.maxGrossExposure * 100)}%`
|
||||
});
|
||||
}
|
||||
|
||||
if (exposure.leverage > this.config.maxLeverage) {
|
||||
violations.push({
|
||||
type: 'maxLeverage',
|
||||
message: `Leverage ${exposure.leverage.toFixed(2)}x exceeds max ${this.config.maxLeverage}x`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: violations.length === 0,
|
||||
violations,
|
||||
currentExposure: this.calculateExposure(currentPositions, portfolioValue),
|
||||
projectedExposure: exposure
|
||||
};
|
||||
}
|
||||
|
||||
// Check cash reserve
|
||||
checkCashReserve(cash, portfolioValue) {
|
||||
const cashPercent = cash / portfolioValue;
|
||||
|
||||
if (cashPercent < this.config.minCashReserve) {
|
||||
return {
|
||||
allowed: false,
|
||||
message: `Cash reserve ${(cashPercent * 100).toFixed(1)}% below min ${(this.config.minCashReserve * 100)}%`,
|
||||
required: this.config.minCashReserve * portfolioValue
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true, cashPercent };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Risk Manager - Main integration class
|
||||
*/
|
||||
class RiskManager {
|
||||
constructor(config = riskConfig) {
|
||||
this.config = config;
|
||||
this.stopLossManager = new StopLossManager(config.stopLoss);
|
||||
this.circuitBreaker = new CircuitBreaker(config.circuitBreakers);
|
||||
this.positionLimits = new PositionLimitManager(config.positions);
|
||||
this.exposureManager = new ExposureManager(config.portfolio);
|
||||
|
||||
// State
|
||||
this.blockedSymbols = new Set();
|
||||
this.dailyLoss = 0;
|
||||
this.dailyStartEquity = 0;
|
||||
}
|
||||
|
||||
// Initialize for trading day
|
||||
startDay(equity) {
|
||||
this.dailyStartEquity = equity;
|
||||
this.dailyLoss = 0;
|
||||
}
|
||||
|
||||
// Main check - can this trade be executed?
|
||||
canTrade(symbol, trade, portfolio) {
|
||||
const results = {
|
||||
allowed: true,
|
||||
checks: {},
|
||||
warnings: [],
|
||||
adjustments: {}
|
||||
};
|
||||
|
||||
// Check circuit breaker
|
||||
const circuitCheck = this.circuitBreaker.canTrade();
|
||||
results.checks.circuitBreaker = circuitCheck;
|
||||
if (!circuitCheck.allowed) {
|
||||
results.allowed = false;
|
||||
return results;
|
||||
}
|
||||
|
||||
// Check if symbol is blocked
|
||||
if (this.blockedSymbols.has(symbol)) {
|
||||
results.allowed = false;
|
||||
results.checks.blocked = { allowed: false, message: `Symbol ${symbol} is blocked` };
|
||||
return results;
|
||||
}
|
||||
|
||||
// Check position limits
|
||||
const positionCheck = this.positionLimits.checkPositionSize(
|
||||
symbol,
|
||||
trade.value,
|
||||
portfolio.equity
|
||||
);
|
||||
results.checks.positionSize = positionCheck;
|
||||
if (!positionCheck.allowed) {
|
||||
results.warnings.push(...positionCheck.violations.map(v => v.message));
|
||||
results.adjustments.size = positionCheck.adjustedSize;
|
||||
}
|
||||
|
||||
// Check position count
|
||||
const countCheck = this.positionLimits.checkPositionCount(portfolio.positions);
|
||||
results.checks.positionCount = countCheck;
|
||||
if (!countCheck.allowed) {
|
||||
results.allowed = false;
|
||||
return results;
|
||||
}
|
||||
|
||||
// Check sector exposure
|
||||
const sectorCheck = this.positionLimits.checkSectorExposure(
|
||||
symbol,
|
||||
trade.value,
|
||||
portfolio.positions,
|
||||
portfolio.equity
|
||||
);
|
||||
results.checks.sectorExposure = sectorCheck;
|
||||
if (!sectorCheck.allowed) {
|
||||
results.warnings.push(sectorCheck.message);
|
||||
}
|
||||
|
||||
// Check portfolio exposure
|
||||
const exposureCheck = this.exposureManager.checkExposure(
|
||||
trade,
|
||||
portfolio.positions,
|
||||
portfolio.equity
|
||||
);
|
||||
results.checks.exposure = exposureCheck;
|
||||
if (!exposureCheck.allowed) {
|
||||
results.allowed = false;
|
||||
return results;
|
||||
}
|
||||
|
||||
// Check cash reserve
|
||||
const cashAfterTrade = portfolio.cash - trade.value;
|
||||
const cashCheck = this.exposureManager.checkCashReserve(cashAfterTrade, portfolio.equity);
|
||||
results.checks.cashReserve = cashCheck;
|
||||
if (!cashCheck.allowed) {
|
||||
results.warnings.push(cashCheck.message);
|
||||
}
|
||||
|
||||
// Check daily loss limit
|
||||
const dailyLossCheck = this.checkDailyLoss(portfolio.equity);
|
||||
results.checks.dailyLoss = dailyLossCheck;
|
||||
if (!dailyLossCheck.allowed) {
|
||||
results.allowed = false;
|
||||
return results;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Check daily loss limit
|
||||
checkDailyLoss(currentEquity) {
|
||||
if (this.dailyStartEquity === 0) return { allowed: true };
|
||||
|
||||
const dailyReturn = (currentEquity - this.dailyStartEquity) / this.dailyStartEquity;
|
||||
|
||||
if (dailyReturn < -this.config.stopLoss.maxDailyLoss) {
|
||||
return {
|
||||
allowed: false,
|
||||
message: `Daily loss ${(Math.abs(dailyReturn) * 100).toFixed(1)}% exceeds max ${(this.config.stopLoss.maxDailyLoss * 100)}%`,
|
||||
dailyLoss: dailyReturn
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true, dailyLoss: dailyReturn };
|
||||
}
|
||||
|
||||
// Set stop-loss for a position
|
||||
setStopLoss(symbol, entryPrice, type, params) {
|
||||
return this.stopLossManager.setStop(symbol, entryPrice, type, params);
|
||||
}
|
||||
|
||||
// Check all stops
|
||||
checkAllStops(prices) {
|
||||
const triggered = [];
|
||||
|
||||
for (const [symbol, price] of Object.entries(prices)) {
|
||||
const check = this.stopLossManager.checkStop(symbol, price);
|
||||
if (check.triggered) {
|
||||
triggered.push({ symbol, ...check });
|
||||
}
|
||||
}
|
||||
|
||||
return triggered;
|
||||
}
|
||||
|
||||
// Update circuit breaker with equity
|
||||
updateEquity(equity) {
|
||||
this.circuitBreaker.updateEquity(equity);
|
||||
}
|
||||
|
||||
// Record trade for circuit breaker
|
||||
recordTrade(profit) {
|
||||
this.circuitBreaker.recordTrade(profit);
|
||||
}
|
||||
|
||||
// Block a symbol
|
||||
blockSymbol(symbol, reason) {
|
||||
this.blockedSymbols.add(symbol);
|
||||
console.warn(`🚫 Symbol ${symbol} blocked: ${reason}`);
|
||||
}
|
||||
|
||||
// Unblock a symbol
|
||||
unblockSymbol(symbol) {
|
||||
this.blockedSymbols.delete(symbol);
|
||||
}
|
||||
|
||||
// Get full risk report
|
||||
getRiskReport(portfolio) {
|
||||
const exposure = this.exposureManager.calculateExposure(portfolio.positions, portfolio.equity);
|
||||
|
||||
return {
|
||||
circuitBreaker: this.circuitBreaker.getState(),
|
||||
exposure,
|
||||
stops: this.stopLossManager.getActiveStops(),
|
||||
blockedSymbols: [...this.blockedSymbols],
|
||||
dailyLoss: this.checkDailyLoss(portfolio.equity),
|
||||
limits: {
|
||||
maxPositionSize: this.config.positions.maxPositionSize,
|
||||
maxLeverage: this.config.portfolio.maxLeverage,
|
||||
maxDrawdown: this.config.circuitBreakers.drawdownThreshold
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Exports
|
||||
export {
|
||||
RiskManager,
|
||||
StopLossManager,
|
||||
CircuitBreaker,
|
||||
PositionLimitManager,
|
||||
ExposureManager,
|
||||
riskConfig
|
||||
};
|
||||
|
||||
// Demo if run directly
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
console.log('RISK MANAGEMENT LAYER');
|
||||
console.log('══════════════════════════════════════════════════════════════════════\n');
|
||||
|
||||
const riskManager = new RiskManager();
|
||||
|
||||
// Initialize for trading day
|
||||
const portfolio = {
|
||||
equity: 100000,
|
||||
cash: 50000,
|
||||
positions: {
|
||||
AAPL: { quantity: 100, price: 150, value: 15000 },
|
||||
MSFT: { quantity: 50, price: 300, value: 15000 }
|
||||
}
|
||||
};
|
||||
|
||||
riskManager.startDay(portfolio.equity);
|
||||
|
||||
console.log('1. Portfolio Status:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
console.log(` Equity: $${portfolio.equity.toLocaleString()}`);
|
||||
console.log(` Cash: $${portfolio.cash.toLocaleString()}`);
|
||||
console.log(` Positions: ${Object.keys(portfolio.positions).length}`);
|
||||
console.log();
|
||||
|
||||
console.log('2. Trade Check - Buy $20,000 GOOGL:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
const trade1 = { symbol: 'GOOGL', side: 'buy', value: 20000, quantity: 100, price: 200 };
|
||||
const check1 = riskManager.canTrade('GOOGL', trade1, portfolio);
|
||||
console.log(` Allowed: ${check1.allowed ? '✓ Yes' : '✗ No'}`);
|
||||
if (check1.warnings.length > 0) {
|
||||
console.log(` Warnings: ${check1.warnings.join(', ')}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
console.log('3. Trade Check - Buy $60,000 TSLA (exceeds limits):');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
const trade2 = { symbol: 'TSLA', side: 'buy', value: 60000, quantity: 300, price: 200 };
|
||||
const check2 = riskManager.canTrade('TSLA', trade2, portfolio);
|
||||
console.log(` Allowed: ${check2.allowed ? '✓ Yes' : '✗ No'}`);
|
||||
if (check2.checks.positionSize?.violations) {
|
||||
for (const v of check2.checks.positionSize.violations) {
|
||||
console.log(` Violation: ${v.message}`);
|
||||
}
|
||||
}
|
||||
if (check2.adjustments.size) {
|
||||
console.log(` Adjusted Size: $${check2.adjustments.size.toLocaleString()}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
console.log('4. Stop-Loss Management:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
const stop = riskManager.setStopLoss('AAPL', 150, 'trailing');
|
||||
console.log(` AAPL trailing stop set at $${stop.stopPrice.toFixed(2)}`);
|
||||
|
||||
// Simulate price movement
|
||||
riskManager.stopLossManager.updateTrailingStop('AAPL', 160); // Price went up
|
||||
const updatedStop = riskManager.stopLossManager.stops.get('AAPL');
|
||||
console.log(` After price rise to $160: stop at $${updatedStop.stopPrice.toFixed(2)}`);
|
||||
|
||||
const stopCheck = riskManager.stopLossManager.checkStop('AAPL', 145); // Price dropped
|
||||
console.log(` Check at $145: ${stopCheck.triggered ? '🔴 TRIGGERED' : '🟢 OK'}`);
|
||||
console.log();
|
||||
|
||||
console.log('5. Circuit Breaker Test:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
// Simulate losses
|
||||
for (let i = 0; i < 4; i++) {
|
||||
riskManager.recordTrade(-500);
|
||||
}
|
||||
console.log(` 4 losing trades recorded`);
|
||||
console.log(` Consecutive losses: ${riskManager.circuitBreaker.consecutiveLosses}`);
|
||||
|
||||
riskManager.recordTrade(-500); // 5th loss
|
||||
const cbState = riskManager.circuitBreaker.getState();
|
||||
console.log(` 5th loss recorded`);
|
||||
console.log(` Circuit breaker: ${cbState.isTripped ? '🔴 TRIPPED' : '🟢 OK'}`);
|
||||
if (cbState.isTripped) {
|
||||
console.log(` Reason: ${cbState.tripMessage}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
console.log('6. Risk Report:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
riskManager.circuitBreaker.forceReset(); // Reset for demo
|
||||
const report = riskManager.getRiskReport(portfolio);
|
||||
console.log(` Long Exposure: ${(report.exposure.long * 100).toFixed(1)}%`);
|
||||
console.log(` Short Exposure: ${(report.exposure.short * 100).toFixed(1)}%`);
|
||||
console.log(` Gross Exposure: ${(report.exposure.gross * 100).toFixed(1)}%`);
|
||||
console.log(` Leverage: ${report.exposure.leverage.toFixed(2)}x`);
|
||||
console.log(` Circuit Breaker: ${report.circuitBreaker.isTripped ? 'TRIPPED' : 'OK'}`);
|
||||
console.log(` Active Stops: ${Object.keys(report.stops).length}`);
|
||||
|
||||
console.log();
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
console.log('Risk management layer ready');
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
}
|
||||
761
vendor/ruvector/examples/neural-trader/system/trading-pipeline.js
vendored
Normal file
761
vendor/ruvector/examples/neural-trader/system/trading-pipeline.js
vendored
Normal file
@@ -0,0 +1,761 @@
|
||||
/**
|
||||
* DAG-Based Trading Pipeline
|
||||
*
|
||||
* Orchestrates all production modules into a unified system:
|
||||
* - LSTM-Transformer for price prediction
|
||||
* - Sentiment Alpha for news signals
|
||||
* - DRL Ensemble for portfolio decisions
|
||||
* - Fractional Kelly for position sizing
|
||||
*
|
||||
* Uses DAG topology for parallel execution and critical path optimization.
|
||||
*/
|
||||
|
||||
import { KellyCriterion, TradingKelly } from '../production/fractional-kelly.js';
|
||||
import { HybridLSTMTransformer, FeatureExtractor } from '../production/hybrid-lstm-transformer.js';
|
||||
import { EnsemblePortfolioManager, PortfolioEnvironment } from '../production/drl-portfolio-manager.js';
|
||||
import { SentimentAggregator, AlphaFactorCalculator, LexiconAnalyzer, EmbeddingAnalyzer } from '../production/sentiment-alpha.js';
|
||||
|
||||
// Pipeline Configuration
|
||||
const pipelineConfig = {
|
||||
// DAG execution settings
|
||||
dag: {
|
||||
parallelExecution: true,
|
||||
maxConcurrency: 4,
|
||||
timeout: 5000, // ms per node
|
||||
retryOnFailure: true,
|
||||
maxRetries: 2
|
||||
},
|
||||
|
||||
// Signal combination weights
|
||||
signalWeights: {
|
||||
lstm: 0.35,
|
||||
sentiment: 0.25,
|
||||
drl: 0.40
|
||||
},
|
||||
|
||||
// Position sizing
|
||||
sizing: {
|
||||
kellyFraction: 'conservative', // 1/5th Kelly
|
||||
maxPositionSize: 0.20, // Max 20% per position
|
||||
minPositionSize: 0.01, // Min 1% position
|
||||
maxTotalExposure: 0.80 // Max 80% invested
|
||||
},
|
||||
|
||||
// Execution settings
|
||||
execution: {
|
||||
slippage: 0.001, // 0.1% slippage assumption
|
||||
commission: 0.001, // 0.1% commission
|
||||
minOrderSize: 100 // Minimum $100 order
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DAG Node - Represents a computation unit in the pipeline
|
||||
*/
|
||||
class DagNode {
|
||||
constructor(id, name, executor, dependencies = []) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.executor = executor;
|
||||
this.dependencies = dependencies;
|
||||
this.status = 'pending'; // pending, running, completed, failed
|
||||
this.result = null;
|
||||
this.error = null;
|
||||
this.startTime = null;
|
||||
this.endTime = null;
|
||||
}
|
||||
|
||||
get latency() {
|
||||
if (!this.startTime || !this.endTime) return null;
|
||||
return this.endTime - this.startTime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trading DAG - Manages pipeline execution
|
||||
*/
|
||||
class TradingDag {
|
||||
constructor(config = pipelineConfig.dag) {
|
||||
this.config = config;
|
||||
this.nodes = new Map();
|
||||
this.edges = new Map(); // node -> dependencies
|
||||
this.results = new Map();
|
||||
this.executionOrder = [];
|
||||
this.metrics = {
|
||||
totalLatency: 0,
|
||||
criticalPath: [],
|
||||
parallelEfficiency: 0
|
||||
};
|
||||
}
|
||||
|
||||
addNode(node) {
|
||||
this.nodes.set(node.id, node);
|
||||
this.edges.set(node.id, node.dependencies);
|
||||
}
|
||||
|
||||
// Topological sort for execution order
|
||||
topologicalSort() {
|
||||
const visited = new Set();
|
||||
const result = [];
|
||||
const visiting = new Set();
|
||||
|
||||
const visit = (nodeId) => {
|
||||
if (visited.has(nodeId)) return;
|
||||
if (visiting.has(nodeId)) {
|
||||
throw new Error(`Cycle detected at node: ${nodeId}`);
|
||||
}
|
||||
|
||||
visiting.add(nodeId);
|
||||
const deps = this.edges.get(nodeId) || [];
|
||||
for (const dep of deps) {
|
||||
visit(dep);
|
||||
}
|
||||
visiting.delete(nodeId);
|
||||
visited.add(nodeId);
|
||||
result.push(nodeId);
|
||||
};
|
||||
|
||||
for (const nodeId of this.nodes.keys()) {
|
||||
visit(nodeId);
|
||||
}
|
||||
|
||||
this.executionOrder = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Find nodes that can execute in parallel
|
||||
getReadyNodes(completed) {
|
||||
const ready = [];
|
||||
for (const [nodeId, deps] of this.edges) {
|
||||
const node = this.nodes.get(nodeId);
|
||||
if (node.status === 'pending') {
|
||||
const allDepsCompleted = deps.every(d => completed.has(d));
|
||||
if (allDepsCompleted) {
|
||||
ready.push(nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ready;
|
||||
}
|
||||
|
||||
// Execute a single node
|
||||
async executeNode(nodeId, context) {
|
||||
const node = this.nodes.get(nodeId);
|
||||
if (!node) throw new Error(`Node not found: ${nodeId}`);
|
||||
|
||||
node.status = 'running';
|
||||
node.startTime = performance.now();
|
||||
|
||||
try {
|
||||
// Gather dependency results
|
||||
const depResults = {};
|
||||
for (const dep of node.dependencies) {
|
||||
depResults[dep] = this.results.get(dep);
|
||||
}
|
||||
|
||||
// Execute with timeout
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), this.config.timeout)
|
||||
);
|
||||
|
||||
const result = await Promise.race([
|
||||
node.executor(context, depResults),
|
||||
timeoutPromise
|
||||
]);
|
||||
|
||||
node.result = result;
|
||||
node.status = 'completed';
|
||||
this.results.set(nodeId, result);
|
||||
} catch (error) {
|
||||
node.error = error;
|
||||
node.status = 'failed';
|
||||
|
||||
if (this.config.retryOnFailure && node.retries < this.config.maxRetries) {
|
||||
node.retries = (node.retries || 0) + 1;
|
||||
node.status = 'pending';
|
||||
return this.executeNode(nodeId, context);
|
||||
}
|
||||
}
|
||||
|
||||
node.endTime = performance.now();
|
||||
return node;
|
||||
}
|
||||
|
||||
// Execute entire DAG
|
||||
async execute(context) {
|
||||
const startTime = performance.now();
|
||||
this.topologicalSort();
|
||||
|
||||
const completed = new Set();
|
||||
const running = new Map();
|
||||
|
||||
while (completed.size < this.nodes.size) {
|
||||
// Get nodes ready to execute
|
||||
const ready = this.getReadyNodes(completed);
|
||||
|
||||
if (ready.length === 0 && running.size === 0) {
|
||||
// Check for failures
|
||||
const failed = [...this.nodes.values()].filter(n => n.status === 'failed');
|
||||
if (failed.length > 0) {
|
||||
throw new Error(`Pipeline failed: ${failed.map(n => n.name).join(', ')}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Execute ready nodes (parallel or sequential)
|
||||
if (this.config.parallelExecution) {
|
||||
const toExecute = ready.slice(0, this.config.maxConcurrency - running.size);
|
||||
const promises = toExecute.map(async nodeId => {
|
||||
running.set(nodeId, true);
|
||||
await this.executeNode(nodeId, context);
|
||||
running.delete(nodeId);
|
||||
completed.add(nodeId);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
} else {
|
||||
for (const nodeId of ready) {
|
||||
await this.executeNode(nodeId, context);
|
||||
completed.add(nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.metrics.totalLatency = performance.now() - startTime;
|
||||
this.computeCriticalPath();
|
||||
|
||||
return this.results;
|
||||
}
|
||||
|
||||
// Compute critical path for optimization insights
|
||||
computeCriticalPath() {
|
||||
const depths = new Map();
|
||||
const latencies = new Map();
|
||||
|
||||
for (const nodeId of this.executionOrder) {
|
||||
const node = this.nodes.get(nodeId);
|
||||
const deps = this.edges.get(nodeId) || [];
|
||||
|
||||
let maxDepth = 0;
|
||||
let maxLatency = 0;
|
||||
for (const dep of deps) {
|
||||
maxDepth = Math.max(maxDepth, (depths.get(dep) || 0) + 1);
|
||||
maxLatency = Math.max(maxLatency, (latencies.get(dep) || 0) + (node.latency || 0));
|
||||
}
|
||||
|
||||
depths.set(nodeId, maxDepth);
|
||||
latencies.set(nodeId, maxLatency + (node.latency || 0));
|
||||
}
|
||||
|
||||
// Find critical path (longest latency)
|
||||
let maxLatency = 0;
|
||||
let criticalEnd = null;
|
||||
for (const [nodeId, latency] of latencies) {
|
||||
if (latency > maxLatency) {
|
||||
maxLatency = latency;
|
||||
criticalEnd = nodeId;
|
||||
}
|
||||
}
|
||||
|
||||
// Trace back critical path
|
||||
this.metrics.criticalPath = [criticalEnd];
|
||||
// Simplified - in production would trace back through dependencies
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
const nodeMetrics = {};
|
||||
for (const [id, node] of this.nodes) {
|
||||
nodeMetrics[id] = {
|
||||
name: node.name,
|
||||
status: node.status,
|
||||
latency: node.latency,
|
||||
error: node.error?.message
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...this.metrics,
|
||||
nodes: nodeMetrics
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified Trading Signal
|
||||
*/
|
||||
class TradingSignal {
|
||||
constructor(symbol, direction, strength, confidence, sources) {
|
||||
this.symbol = symbol;
|
||||
this.direction = direction; // 'long', 'short', 'neutral'
|
||||
this.strength = strength; // 0-1
|
||||
this.confidence = confidence; // 0-1
|
||||
this.sources = sources; // { lstm, sentiment, drl }
|
||||
this.timestamp = Date.now();
|
||||
}
|
||||
|
||||
get score() {
|
||||
return this.direction === 'long' ? this.strength :
|
||||
this.direction === 'short' ? -this.strength : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trading Pipeline - Main integration class
|
||||
*/
|
||||
class TradingPipeline {
|
||||
constructor(config = pipelineConfig) {
|
||||
this.config = config;
|
||||
|
||||
// Initialize production modules
|
||||
this.kelly = new TradingKelly();
|
||||
this.featureExtractor = new FeatureExtractor();
|
||||
this.lstmTransformer = null; // Lazy init with correct dimensions
|
||||
this.sentimentAggregator = new SentimentAggregator();
|
||||
this.alphaCalculator = new AlphaFactorCalculator(this.sentimentAggregator);
|
||||
this.lexicon = new LexiconAnalyzer();
|
||||
this.embedding = new EmbeddingAnalyzer();
|
||||
|
||||
// DRL initialized per portfolio
|
||||
this.drlManager = null;
|
||||
|
||||
// Pipeline state
|
||||
this.positions = new Map();
|
||||
this.signals = new Map();
|
||||
this.orders = [];
|
||||
}
|
||||
|
||||
// Build the execution DAG
|
||||
buildDag() {
|
||||
const dag = new TradingDag(this.config.dag);
|
||||
|
||||
// Node 1: Data Preparation
|
||||
dag.addNode(new DagNode('data_prep', 'Data Preparation', async (ctx) => {
|
||||
const { marketData, newsData } = ctx;
|
||||
return {
|
||||
candles: marketData,
|
||||
news: newsData,
|
||||
features: this.featureExtractor.extract(marketData)
|
||||
};
|
||||
}, []));
|
||||
|
||||
// Node 2: LSTM-Transformer Prediction (depends on data_prep)
|
||||
dag.addNode(new DagNode('lstm_predict', 'LSTM Prediction', async (ctx, deps) => {
|
||||
const { features } = deps.data_prep;
|
||||
if (!features || features.length === 0) {
|
||||
return { prediction: 0, confidence: 0, signal: 'HOLD' };
|
||||
}
|
||||
|
||||
// Lazy init LSTM with correct input size
|
||||
if (!this.lstmTransformer) {
|
||||
const inputSize = features[0]?.length || 10;
|
||||
this.lstmTransformer = new HybridLSTMTransformer({
|
||||
lstm: { inputSize, hiddenSize: 64, numLayers: 2 },
|
||||
transformer: { dModel: 64, numHeads: 4, numLayers: 2, ffDim: 128 }
|
||||
});
|
||||
}
|
||||
|
||||
const prediction = this.lstmTransformer.predict(features);
|
||||
return prediction;
|
||||
}, ['data_prep']));
|
||||
|
||||
// Node 3: Sentiment Analysis (depends on data_prep, parallel with LSTM)
|
||||
dag.addNode(new DagNode('sentiment_analyze', 'Sentiment Analysis', async (ctx, deps) => {
|
||||
const { news } = deps.data_prep;
|
||||
if (!news || news.length === 0) {
|
||||
return { score: 0, confidence: 0, signal: 'HOLD' };
|
||||
}
|
||||
|
||||
// Analyze each news item
|
||||
for (const item of news) {
|
||||
const lexiconResult = this.lexicon.analyze(item.text);
|
||||
const embeddingResult = this.embedding.analyze(item.text);
|
||||
|
||||
this.sentimentAggregator.addSentiment(item.symbol, {
|
||||
source: item.source || 'news',
|
||||
score: (lexiconResult.score + embeddingResult.score) / 2,
|
||||
confidence: (lexiconResult.confidence + embeddingResult.confidence) / 2,
|
||||
timestamp: item.timestamp || Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// Get aggregated sentiment per symbol
|
||||
const symbols = [...new Set(news.map(n => n.symbol))];
|
||||
const sentiments = {};
|
||||
for (const symbol of symbols) {
|
||||
sentiments[symbol] = this.sentimentAggregator.getAggregatedSentiment(symbol);
|
||||
sentiments[symbol].alpha = this.alphaCalculator.calculateAlpha(symbol);
|
||||
}
|
||||
|
||||
return sentiments;
|
||||
}, ['data_prep']));
|
||||
|
||||
// Node 4: DRL Portfolio Decision (depends on data_prep, parallel with LSTM/Sentiment)
|
||||
dag.addNode(new DagNode('drl_decide', 'DRL Decision', async (ctx, deps) => {
|
||||
const { candles } = deps.data_prep;
|
||||
const { portfolio } = ctx;
|
||||
|
||||
if (!portfolio || !candles || candles.length === 0) {
|
||||
return { weights: [], action: 'hold' };
|
||||
}
|
||||
|
||||
// Initialize DRL if needed
|
||||
if (!this.drlManager) {
|
||||
const numAssets = portfolio.assets?.length || 1;
|
||||
this.drlManager = new EnsemblePortfolioManager(numAssets, {
|
||||
lookbackWindow: 30,
|
||||
transactionCost: 0.001
|
||||
});
|
||||
}
|
||||
|
||||
// Get state from environment
|
||||
const state = this.buildDrlState(candles, portfolio);
|
||||
const action = this.drlManager.getEnsembleAction(state);
|
||||
|
||||
return {
|
||||
weights: action,
|
||||
action: this.interpretDrlAction(action)
|
||||
};
|
||||
}, ['data_prep']));
|
||||
|
||||
// Node 5: Signal Fusion (depends on lstm, sentiment, drl)
|
||||
dag.addNode(new DagNode('signal_fusion', 'Signal Fusion', async (ctx, deps) => {
|
||||
const lstmResult = deps.lstm_predict;
|
||||
const sentimentResult = deps.sentiment_analyze;
|
||||
const drlResult = deps.drl_decide;
|
||||
const { symbols } = ctx;
|
||||
|
||||
const signals = {};
|
||||
|
||||
for (const symbol of (symbols || ['DEFAULT'])) {
|
||||
// Get individual signals
|
||||
const lstmSignal = this.normalizeSignal(lstmResult);
|
||||
const sentimentSignal = this.normalizeSentiment(sentimentResult[symbol]);
|
||||
const drlSignal = this.normalizeDrl(drlResult, symbol);
|
||||
|
||||
// Weighted combination
|
||||
const w = this.config.signalWeights;
|
||||
const combinedScore =
|
||||
w.lstm * lstmSignal.score +
|
||||
w.sentiment * sentimentSignal.score +
|
||||
w.drl * drlSignal.score;
|
||||
|
||||
const combinedConfidence =
|
||||
w.lstm * lstmSignal.confidence +
|
||||
w.sentiment * sentimentSignal.confidence +
|
||||
w.drl * drlSignal.confidence;
|
||||
|
||||
const direction = combinedScore > 0.1 ? 'long' :
|
||||
combinedScore < -0.1 ? 'short' : 'neutral';
|
||||
|
||||
signals[symbol] = new TradingSignal(
|
||||
symbol,
|
||||
direction,
|
||||
Math.abs(combinedScore),
|
||||
combinedConfidence,
|
||||
{ lstm: lstmSignal, sentiment: sentimentSignal, drl: drlSignal }
|
||||
);
|
||||
}
|
||||
|
||||
return signals;
|
||||
}, ['lstm_predict', 'sentiment_analyze', 'drl_decide']));
|
||||
|
||||
// Node 6: Position Sizing with Kelly (depends on signal_fusion)
|
||||
dag.addNode(new DagNode('position_sizing', 'Position Sizing', async (ctx, deps) => {
|
||||
const signals = deps.signal_fusion;
|
||||
const { portfolio, riskManager } = ctx;
|
||||
|
||||
const positions = {};
|
||||
|
||||
for (const [symbol, signal] of Object.entries(signals)) {
|
||||
if (signal.direction === 'neutral') {
|
||||
positions[symbol] = { size: 0, action: 'hold' };
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check risk limits first
|
||||
if (riskManager && !riskManager.canTrade(symbol)) {
|
||||
positions[symbol] = { size: 0, action: 'blocked', reason: 'risk_limit' };
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate Kelly position size
|
||||
const winProb = 0.5 + signal.strength * signal.confidence * 0.2; // Map to 0.5-0.7
|
||||
const avgWin = 0.02; // 2% average win
|
||||
const avgLoss = 0.015; // 1.5% average loss
|
||||
|
||||
const kellyResult = this.kelly.calculatePositionSize(
|
||||
portfolio?.equity || 10000,
|
||||
winProb,
|
||||
avgWin,
|
||||
avgLoss,
|
||||
this.config.sizing.kellyFraction
|
||||
);
|
||||
|
||||
// Apply position limits
|
||||
let size = kellyResult.positionSize;
|
||||
size = Math.min(size, portfolio?.equity * this.config.sizing.maxPositionSize);
|
||||
size = Math.max(size, portfolio?.equity * this.config.sizing.minPositionSize);
|
||||
|
||||
// Check total exposure
|
||||
const currentExposure = this.calculateExposure(positions, portfolio);
|
||||
if (currentExposure + size / portfolio?.equity > this.config.sizing.maxTotalExposure) {
|
||||
size = (this.config.sizing.maxTotalExposure - currentExposure) * portfolio?.equity;
|
||||
}
|
||||
|
||||
positions[symbol] = {
|
||||
size: signal.direction === 'short' ? -size : size,
|
||||
action: signal.direction === 'long' ? 'buy' : 'sell',
|
||||
kelly: kellyResult,
|
||||
signal
|
||||
};
|
||||
}
|
||||
|
||||
return positions;
|
||||
}, ['signal_fusion']));
|
||||
|
||||
// Node 7: Order Generation (depends on position_sizing)
|
||||
dag.addNode(new DagNode('order_gen', 'Order Generation', async (ctx, deps) => {
|
||||
const positions = deps.position_sizing;
|
||||
const { portfolio, prices } = ctx;
|
||||
|
||||
const orders = [];
|
||||
|
||||
for (const [symbol, position] of Object.entries(positions)) {
|
||||
if (position.action === 'hold' || position.action === 'blocked') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPosition = portfolio?.positions?.[symbol] || 0;
|
||||
const targetPosition = position.size;
|
||||
const delta = targetPosition - currentPosition;
|
||||
|
||||
if (Math.abs(delta) < this.config.execution.minOrderSize) {
|
||||
continue; // Skip small orders
|
||||
}
|
||||
|
||||
const price = prices?.[symbol] || 100;
|
||||
const shares = Math.floor(Math.abs(delta) / price);
|
||||
|
||||
if (shares > 0) {
|
||||
orders.push({
|
||||
symbol,
|
||||
side: delta > 0 ? 'buy' : 'sell',
|
||||
quantity: shares,
|
||||
type: 'market',
|
||||
price,
|
||||
estimatedValue: shares * price,
|
||||
slippage: shares * price * this.config.execution.slippage,
|
||||
commission: shares * price * this.config.execution.commission,
|
||||
signal: position.signal,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return orders;
|
||||
}, ['position_sizing']));
|
||||
|
||||
return dag;
|
||||
}
|
||||
|
||||
// Helper: Build DRL state vector
|
||||
buildDrlState(candles, portfolio) {
|
||||
const state = [];
|
||||
|
||||
// Price features (last 30 returns)
|
||||
const returns = [];
|
||||
for (let i = 1; i < Math.min(31, candles.length); i++) {
|
||||
returns.push((candles[i].close - candles[i-1].close) / candles[i-1].close);
|
||||
}
|
||||
state.push(...returns);
|
||||
|
||||
// Portfolio features
|
||||
if (portfolio) {
|
||||
state.push(portfolio.cash / portfolio.equity);
|
||||
state.push(portfolio.exposure || 0);
|
||||
}
|
||||
|
||||
// Pad to expected size
|
||||
while (state.length < 62) state.push(0);
|
||||
|
||||
return state.slice(0, 62);
|
||||
}
|
||||
|
||||
// Helper: Normalize LSTM output to signal
|
||||
normalizeSignal(lstmResult) {
|
||||
if (!lstmResult) return { score: 0, confidence: 0 };
|
||||
return {
|
||||
score: lstmResult.prediction || 0,
|
||||
confidence: lstmResult.confidence || 0
|
||||
};
|
||||
}
|
||||
|
||||
// Helper: Normalize sentiment to signal
|
||||
normalizeSentiment(sentiment) {
|
||||
if (!sentiment) return { score: 0, confidence: 0 };
|
||||
return {
|
||||
score: sentiment.score || sentiment.alpha?.factor || 0,
|
||||
confidence: sentiment.confidence || 0.5
|
||||
};
|
||||
}
|
||||
|
||||
// Helper: Normalize DRL output to signal
|
||||
normalizeDrl(drlResult, symbol) {
|
||||
if (!drlResult || !drlResult.weights) return { score: 0, confidence: 0 };
|
||||
// Map weight to signal (-1 to 1)
|
||||
const weight = drlResult.weights[0] || 0;
|
||||
return {
|
||||
score: weight * 2 - 1, // Map 0-1 to -1 to 1
|
||||
confidence: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
// Helper: Calculate current exposure
|
||||
calculateExposure(positions, portfolio) {
|
||||
if (!portfolio?.equity) return 0;
|
||||
let exposure = 0;
|
||||
for (const pos of Object.values(positions)) {
|
||||
exposure += Math.abs(pos.size || 0);
|
||||
}
|
||||
return exposure / portfolio.equity;
|
||||
}
|
||||
|
||||
// Main execution method
|
||||
async execute(context) {
|
||||
const dag = this.buildDag();
|
||||
const results = await dag.execute(context);
|
||||
|
||||
return {
|
||||
signals: results.get('signal_fusion'),
|
||||
positions: results.get('position_sizing'),
|
||||
orders: results.get('order_gen'),
|
||||
metrics: dag.getMetrics()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipeline Factory
|
||||
*/
|
||||
function createTradingPipeline(config) {
|
||||
return new TradingPipeline({ ...pipelineConfig, ...config });
|
||||
}
|
||||
|
||||
export {
|
||||
TradingPipeline,
|
||||
TradingDag,
|
||||
DagNode,
|
||||
TradingSignal,
|
||||
createTradingPipeline,
|
||||
pipelineConfig
|
||||
};
|
||||
|
||||
// Demo if run directly
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
console.log('DAG-BASED TRADING PIPELINE');
|
||||
console.log('══════════════════════════════════════════════════════════════════════\n');
|
||||
|
||||
const pipeline = createTradingPipeline();
|
||||
|
||||
// Generate sample data
|
||||
const generateCandles = (n) => {
|
||||
const candles = [];
|
||||
let price = 100;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const change = (Math.random() - 0.5) * 2;
|
||||
price *= (1 + change / 100);
|
||||
candles.push({
|
||||
open: price * (1 - Math.random() * 0.01),
|
||||
high: price * (1 + Math.random() * 0.02),
|
||||
low: price * (1 - Math.random() * 0.02),
|
||||
close: price,
|
||||
volume: 1000000 * (0.5 + Math.random())
|
||||
});
|
||||
}
|
||||
return candles;
|
||||
};
|
||||
|
||||
const context = {
|
||||
marketData: generateCandles(100),
|
||||
newsData: [
|
||||
{ symbol: 'AAPL', text: 'Strong earnings beat expectations with record revenue growth', source: 'news', timestamp: Date.now() },
|
||||
{ symbol: 'AAPL', text: 'Analysts upgrade rating after impressive quarterly results', source: 'analyst', timestamp: Date.now() },
|
||||
{ symbol: 'AAPL', text: 'New product launch receives positive market reception', source: 'social', timestamp: Date.now() }
|
||||
],
|
||||
symbols: ['AAPL'],
|
||||
portfolio: {
|
||||
equity: 100000,
|
||||
cash: 50000,
|
||||
positions: {},
|
||||
exposure: 0,
|
||||
assets: ['AAPL']
|
||||
},
|
||||
prices: { AAPL: 150 },
|
||||
riskManager: null
|
||||
};
|
||||
|
||||
console.log('1. Pipeline Configuration:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
console.log(` Parallel execution: ${pipelineConfig.dag.parallelExecution}`);
|
||||
console.log(` Max concurrency: ${pipelineConfig.dag.maxConcurrency}`);
|
||||
console.log(` Signal weights: LSTM=${pipelineConfig.signalWeights.lstm}, Sentiment=${pipelineConfig.signalWeights.sentiment}, DRL=${pipelineConfig.signalWeights.drl}`);
|
||||
console.log(` Kelly fraction: ${pipelineConfig.sizing.kellyFraction}`);
|
||||
console.log();
|
||||
|
||||
console.log('2. Executing Pipeline:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
|
||||
pipeline.execute(context).then(result => {
|
||||
console.log(` Total latency: ${result.metrics.totalLatency.toFixed(2)}ms`);
|
||||
console.log();
|
||||
|
||||
console.log('3. Node Execution:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
for (const [id, node] of Object.entries(result.metrics.nodes)) {
|
||||
const status = node.status === 'completed' ? '✓' : '✗';
|
||||
console.log(` ${status} ${node.name.padEnd(20)} ${(node.latency || 0).toFixed(2)}ms`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
console.log('4. Trading Signals:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
for (const [symbol, signal] of Object.entries(result.signals || {})) {
|
||||
console.log(` ${symbol}: ${signal.direction.toUpperCase()} (strength: ${(signal.strength * 100).toFixed(1)}%, confidence: ${(signal.confidence * 100).toFixed(1)}%)`);
|
||||
console.log(` LSTM: ${(signal.sources.lstm.score * 100).toFixed(1)}%`);
|
||||
console.log(` Sentiment: ${(signal.sources.sentiment.score * 100).toFixed(1)}%`);
|
||||
console.log(` DRL: ${(signal.sources.drl.score * 100).toFixed(1)}%`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
console.log('5. Position Sizing:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
for (const [symbol, pos] of Object.entries(result.positions || {})) {
|
||||
if (pos.action !== 'hold') {
|
||||
console.log(` ${symbol}: ${pos.action.toUpperCase()} $${Math.abs(pos.size).toFixed(2)}`);
|
||||
if (pos.kelly) {
|
||||
console.log(` Kelly: ${(pos.kelly.kellyFraction * 100).toFixed(2)}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
|
||||
console.log('6. Generated Orders:');
|
||||
console.log('──────────────────────────────────────────────────────────────────────');
|
||||
if (result.orders && result.orders.length > 0) {
|
||||
for (const order of result.orders) {
|
||||
console.log(` ${order.side.toUpperCase()} ${order.quantity} ${order.symbol} @ $${order.price.toFixed(2)}`);
|
||||
console.log(` Value: $${order.estimatedValue.toFixed(2)}, Costs: $${(order.slippage + order.commission).toFixed(2)}`);
|
||||
}
|
||||
} else {
|
||||
console.log(' No orders generated');
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
console.log('Trading pipeline execution completed');
|
||||
console.log('══════════════════════════════════════════════════════════════════════');
|
||||
}).catch(err => {
|
||||
console.error('Pipeline error:', err);
|
||||
});
|
||||
}
|
||||
478
vendor/ruvector/examples/neural-trader/system/visualization.js
vendored
Normal file
478
vendor/ruvector/examples/neural-trader/system/visualization.js
vendored
Normal file
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* Visualization Module
|
||||
*
|
||||
* Terminal-based charts for equity curves, signals, and metrics
|
||||
* Uses ASCII art for compatibility across all terminals
|
||||
*/
|
||||
|
||||
// Chart Configuration
|
||||
const chartConfig = {
|
||||
width: 80,
|
||||
height: 20,
|
||||
padding: { left: 10, right: 2, top: 1, bottom: 3 },
|
||||
colors: {
|
||||
positive: '\x1b[32m',
|
||||
negative: '\x1b[31m',
|
||||
neutral: '\x1b[33m',
|
||||
reset: '\x1b[0m',
|
||||
dim: '\x1b[2m',
|
||||
bold: '\x1b[1m'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ASCII Line Chart
|
||||
*/
|
||||
class LineChart {
|
||||
constructor(config = {}) {
|
||||
this.width = config.width || chartConfig.width;
|
||||
this.height = config.height || chartConfig.height;
|
||||
this.padding = { ...chartConfig.padding, ...config.padding };
|
||||
}
|
||||
|
||||
render(data, options = {}) {
|
||||
const { title = 'Chart', showGrid = true, colored = true } = options;
|
||||
|
||||
if (!data || data.length === 0) return 'No data to display';
|
||||
|
||||
const plotWidth = this.width - this.padding.left - this.padding.right;
|
||||
const plotHeight = this.height - this.padding.top - this.padding.bottom;
|
||||
|
||||
// Calculate min/max
|
||||
const values = data.map(d => typeof d === 'number' ? d : d.value);
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const range = max - min || 1;
|
||||
|
||||
// Create canvas
|
||||
const canvas = [];
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
canvas.push(new Array(this.width).fill(' '));
|
||||
}
|
||||
|
||||
// Draw title
|
||||
const titleStr = ` ${title} `;
|
||||
const titleStart = Math.floor((this.width - titleStr.length) / 2);
|
||||
for (let i = 0; i < titleStr.length; i++) {
|
||||
canvas[0][titleStart + i] = titleStr[i];
|
||||
}
|
||||
|
||||
// Draw Y-axis labels
|
||||
for (let y = 0; y < plotHeight; y++) {
|
||||
const value = max - (y / (plotHeight - 1)) * range;
|
||||
const label = this.formatNumber(value).padStart(this.padding.left - 1);
|
||||
for (let i = 0; i < label.length; i++) {
|
||||
canvas[this.padding.top + y][i] = label[i];
|
||||
}
|
||||
canvas[this.padding.top + y][this.padding.left - 1] = '│';
|
||||
}
|
||||
|
||||
// Draw X-axis
|
||||
for (let x = this.padding.left; x < this.width - this.padding.right; x++) {
|
||||
canvas[this.height - this.padding.bottom][x] = '─';
|
||||
}
|
||||
canvas[this.height - this.padding.bottom][this.padding.left - 1] = '└';
|
||||
|
||||
// Plot data points
|
||||
const step = Math.max(1, Math.floor(data.length / plotWidth));
|
||||
let prevY = null;
|
||||
|
||||
for (let i = 0; i < plotWidth && i * step < data.length; i++) {
|
||||
const idx = Math.min(i * step, data.length - 1);
|
||||
const value = values[idx];
|
||||
const normalizedY = (max - value) / range;
|
||||
const y = Math.floor(normalizedY * (plotHeight - 1));
|
||||
const x = this.padding.left + i;
|
||||
|
||||
// Draw point
|
||||
const chartY = this.padding.top + y;
|
||||
if (chartY >= 0 && chartY < this.height) {
|
||||
if (prevY !== null && Math.abs(y - prevY) > 1) {
|
||||
// Draw connecting lines for gaps
|
||||
const startY = Math.min(y, prevY);
|
||||
const endY = Math.max(y, prevY);
|
||||
for (let cy = startY; cy <= endY; cy++) {
|
||||
const connectY = this.padding.top + cy;
|
||||
if (connectY >= 0 && connectY < this.height) {
|
||||
canvas[connectY][x - 1] = '│';
|
||||
}
|
||||
}
|
||||
}
|
||||
canvas[chartY][x] = '●';
|
||||
}
|
||||
prevY = y;
|
||||
}
|
||||
|
||||
// Add grid if enabled
|
||||
if (showGrid) {
|
||||
for (let y = this.padding.top; y < this.height - this.padding.bottom; y += 4) {
|
||||
for (let x = this.padding.left; x < this.width - this.padding.right; x += 10) {
|
||||
if (canvas[y][x] === ' ') {
|
||||
canvas[y][x] = '·';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to string with colors
|
||||
let result = '';
|
||||
const c = colored ? chartConfig.colors : { positive: '', negative: '', neutral: '', reset: '', dim: '', bold: '' };
|
||||
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
let line = '';
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
const char = canvas[y][x];
|
||||
if (char === '●') {
|
||||
const dataIdx = Math.floor((x - this.padding.left) * step);
|
||||
const value = values[Math.min(dataIdx, values.length - 1)];
|
||||
const prevValue = dataIdx > 0 ? values[dataIdx - 1] : value;
|
||||
const color = value >= prevValue ? c.positive : c.negative;
|
||||
line += color + char + c.reset;
|
||||
} else if (char === '·') {
|
||||
line += c.dim + char + c.reset;
|
||||
} else {
|
||||
line += char;
|
||||
}
|
||||
}
|
||||
result += line + '\n';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
formatNumber(n) {
|
||||
if (Math.abs(n) >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
||||
if (Math.abs(n) >= 1000) return (n / 1000).toFixed(1) + 'K';
|
||||
return n.toFixed(Math.abs(n) < 10 ? 2 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bar Chart (for returns, volume, etc.)
|
||||
*/
|
||||
class BarChart {
|
||||
constructor(config = {}) {
|
||||
this.width = config.width || 60;
|
||||
this.height = config.height || 15;
|
||||
this.barWidth = config.barWidth || 1;
|
||||
}
|
||||
|
||||
render(data, options = {}) {
|
||||
const { title = 'Bar Chart', labels = [], colored = true } = options;
|
||||
|
||||
if (!data || data.length === 0) return 'No data to display';
|
||||
|
||||
const values = data.map(d => typeof d === 'number' ? d : d.value);
|
||||
const maxVal = Math.max(...values.map(Math.abs));
|
||||
const hasNegative = values.some(v => v < 0);
|
||||
|
||||
const c = colored ? chartConfig.colors : { positive: '', negative: '', neutral: '', reset: '' };
|
||||
let result = '';
|
||||
|
||||
// Title
|
||||
result += `\n ${title}\n`;
|
||||
result += ' ' + '─'.repeat(this.width) + '\n';
|
||||
|
||||
if (hasNegative) {
|
||||
// Diverging bar chart
|
||||
const midLine = Math.floor(this.height / 2);
|
||||
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
let line = ' ';
|
||||
const threshold = maxVal * (1 - (y / this.height) * 2);
|
||||
|
||||
for (let i = 0; i < Math.min(values.length, this.width); i++) {
|
||||
const v = values[i];
|
||||
const normalizedV = v / maxVal;
|
||||
|
||||
if (y < midLine && v > 0 && normalizedV >= (midLine - y) / midLine) {
|
||||
line += c.positive + '█' + c.reset;
|
||||
} else if (y > midLine && v < 0 && Math.abs(normalizedV) >= (y - midLine) / midLine) {
|
||||
line += c.negative + '█' + c.reset;
|
||||
} else if (y === midLine) {
|
||||
line += '─';
|
||||
} else {
|
||||
line += ' ';
|
||||
}
|
||||
}
|
||||
result += line + '\n';
|
||||
}
|
||||
} else {
|
||||
// Standard bar chart
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
let line = ' ';
|
||||
const threshold = maxVal * (1 - y / this.height);
|
||||
|
||||
for (let i = 0; i < Math.min(values.length, this.width); i++) {
|
||||
const v = values[i];
|
||||
if (v >= threshold) {
|
||||
line += c.positive + '█' + c.reset;
|
||||
} else {
|
||||
line += ' ';
|
||||
}
|
||||
}
|
||||
result += line + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// X-axis labels
|
||||
if (labels.length > 0) {
|
||||
result += ' ' + labels.slice(0, this.width).map(l => l[0] || ' ').join('') + '\n';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sparkline (inline mini chart)
|
||||
*/
|
||||
class Sparkline {
|
||||
static render(data, options = {}) {
|
||||
const { width = 20, colored = true } = options;
|
||||
const chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||
|
||||
if (!data || data.length === 0) return '';
|
||||
|
||||
const values = data.slice(-width);
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const range = max - min || 1;
|
||||
|
||||
const c = colored ? chartConfig.colors : { positive: '', negative: '', reset: '' };
|
||||
let result = '';
|
||||
let prev = values[0];
|
||||
|
||||
for (const v of values) {
|
||||
const normalized = (v - min) / range;
|
||||
const idx = Math.min(Math.floor(normalized * chars.length), chars.length - 1);
|
||||
const color = v >= prev ? c.positive : c.negative;
|
||||
result += color + chars[idx] + c.reset;
|
||||
prev = v;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Table Renderer
|
||||
*/
|
||||
class Table {
|
||||
static render(data, options = {}) {
|
||||
const { headers = [], title = '' } = options;
|
||||
|
||||
if (!data || data.length === 0) return 'No data';
|
||||
|
||||
// Calculate column widths
|
||||
const allRows = headers.length > 0 ? [headers, ...data] : data;
|
||||
const numCols = Math.max(...allRows.map(r => r.length));
|
||||
const colWidths = new Array(numCols).fill(0);
|
||||
|
||||
for (const row of allRows) {
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
colWidths[i] = Math.max(colWidths[i], String(row[i]).length);
|
||||
}
|
||||
}
|
||||
|
||||
const totalWidth = colWidths.reduce((a, b) => a + b, 0) + (numCols * 3) + 1;
|
||||
let result = '';
|
||||
|
||||
// Title
|
||||
if (title) {
|
||||
result += '\n ' + title + '\n';
|
||||
}
|
||||
|
||||
// Top border
|
||||
result += ' ┌' + colWidths.map(w => '─'.repeat(w + 2)).join('┬') + '┐\n';
|
||||
|
||||
// Headers
|
||||
if (headers.length > 0) {
|
||||
result += ' │';
|
||||
for (let i = 0; i < numCols; i++) {
|
||||
const cell = String(headers[i] || '').padEnd(colWidths[i]);
|
||||
result += ` ${cell} │`;
|
||||
}
|
||||
result += '\n';
|
||||
result += ' ├' + colWidths.map(w => '─'.repeat(w + 2)).join('┼') + '┤\n';
|
||||
}
|
||||
|
||||
// Data rows
|
||||
for (const row of data) {
|
||||
result += ' │';
|
||||
for (let i = 0; i < numCols; i++) {
|
||||
const cell = String(row[i] || '').padEnd(colWidths[i]);
|
||||
result += ` ${cell} │`;
|
||||
}
|
||||
result += '\n';
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
result += ' └' + colWidths.map(w => '─'.repeat(w + 2)).join('┴') + '┘\n';
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard - Combines multiple visualizations
|
||||
*/
|
||||
class Dashboard {
|
||||
constructor(title = 'Trading Dashboard') {
|
||||
this.title = title;
|
||||
this.panels = [];
|
||||
}
|
||||
|
||||
addPanel(content, options = {}) {
|
||||
this.panels.push({ content, ...options });
|
||||
return this;
|
||||
}
|
||||
|
||||
addEquityCurve(data, title = 'Equity Curve') {
|
||||
const chart = new LineChart({ width: 70, height: 12 });
|
||||
return this.addPanel(chart.render(data, { title }));
|
||||
}
|
||||
|
||||
addReturnsBar(returns, title = 'Daily Returns') {
|
||||
const chart = new BarChart({ width: 50, height: 8 });
|
||||
return this.addPanel(chart.render(returns.slice(-50), { title }));
|
||||
}
|
||||
|
||||
addMetricsTable(metrics) {
|
||||
const data = [
|
||||
['Total Return', `${(metrics.totalReturn * 100).toFixed(2)}%`],
|
||||
['Sharpe Ratio', metrics.sharpeRatio.toFixed(2)],
|
||||
['Max Drawdown', `${(metrics.maxDrawdown * 100).toFixed(2)}%`],
|
||||
['Win Rate', `${(metrics.winRate * 100).toFixed(1)}%`],
|
||||
['Profit Factor', metrics.profitFactor.toFixed(2)]
|
||||
];
|
||||
return this.addPanel(Table.render(data, { headers: ['Metric', 'Value'], title: 'Performance' }));
|
||||
}
|
||||
|
||||
addSignals(signals) {
|
||||
const c = chartConfig.colors;
|
||||
let content = '\n SIGNALS\n ' + '─'.repeat(40) + '\n';
|
||||
|
||||
for (const [symbol, signal] of Object.entries(signals)) {
|
||||
const color = signal.direction === 'long' ? c.positive :
|
||||
signal.direction === 'short' ? c.negative : c.neutral;
|
||||
const arrow = signal.direction === 'long' ? '▲' :
|
||||
signal.direction === 'short' ? '▼' : '●';
|
||||
content += ` ${color}${arrow}${c.reset} ${symbol.padEnd(6)} ${signal.direction.toUpperCase().padEnd(6)} `;
|
||||
content += `${(signal.strength * 100).toFixed(0)}% confidence\n`;
|
||||
}
|
||||
|
||||
return this.addPanel(content);
|
||||
}
|
||||
|
||||
render() {
|
||||
const c = chartConfig.colors;
|
||||
let result = '\n';
|
||||
result += c.bold + '═'.repeat(80) + c.reset + '\n';
|
||||
result += c.bold + ' '.repeat((80 - this.title.length) / 2) + this.title + c.reset + '\n';
|
||||
result += c.bold + '═'.repeat(80) + c.reset + '\n';
|
||||
|
||||
for (const panel of this.panels) {
|
||||
result += panel.content;
|
||||
result += '\n';
|
||||
}
|
||||
|
||||
result += c.dim + '─'.repeat(80) + c.reset + '\n';
|
||||
result += c.dim + `Generated at ${new Date().toLocaleString()}` + c.reset + '\n';
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick visualization helpers
|
||||
*/
|
||||
const viz = {
|
||||
// Quick equity curve
|
||||
equity: (data, title = 'Equity Curve') => {
|
||||
const chart = new LineChart();
|
||||
return chart.render(data, { title });
|
||||
},
|
||||
|
||||
// Quick returns bar
|
||||
returns: (data, title = 'Returns') => {
|
||||
const chart = new BarChart();
|
||||
return chart.render(data, { title });
|
||||
},
|
||||
|
||||
// Inline sparkline
|
||||
spark: (data) => Sparkline.render(data),
|
||||
|
||||
// Quick table
|
||||
table: (data, headers) => Table.render(data, { headers }),
|
||||
|
||||
// Progress bar
|
||||
progress: (current, total, width = 30) => {
|
||||
const pct = current / total;
|
||||
const filled = Math.floor(pct * width);
|
||||
const empty = width - filled;
|
||||
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${(pct * 100).toFixed(1)}%`;
|
||||
},
|
||||
|
||||
// Status indicator
|
||||
status: (value, thresholds = { good: 0, warn: -0.05, bad: -0.1 }) => {
|
||||
const c = chartConfig.colors;
|
||||
if (value >= thresholds.good) return c.positive + '●' + c.reset;
|
||||
if (value >= thresholds.warn) return c.neutral + '●' + c.reset;
|
||||
return c.negative + '●' + c.reset;
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
LineChart,
|
||||
BarChart,
|
||||
Sparkline,
|
||||
Table,
|
||||
Dashboard,
|
||||
viz,
|
||||
chartConfig
|
||||
};
|
||||
|
||||
// Demo if run directly
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
console.log('═'.repeat(70));
|
||||
console.log('VISUALIZATION MODULE DEMO');
|
||||
console.log('═'.repeat(70));
|
||||
|
||||
// Generate sample data
|
||||
const equityData = [100000];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
equityData.push(equityData[i] * (1 + (Math.random() - 0.48) * 0.02));
|
||||
}
|
||||
|
||||
const returns = [];
|
||||
for (let i = 1; i < equityData.length; i++) {
|
||||
returns.push((equityData[i] - equityData[i-1]) / equityData[i-1]);
|
||||
}
|
||||
|
||||
// Line chart
|
||||
const lineChart = new LineChart();
|
||||
console.log(lineChart.render(equityData, { title: 'Portfolio Equity' }));
|
||||
|
||||
// Sparkline
|
||||
console.log('Sparkline: ' + Sparkline.render(equityData.slice(-30)));
|
||||
console.log();
|
||||
|
||||
// Table
|
||||
console.log(Table.render([
|
||||
['AAPL', '+2.5%', '150.25', 'BUY'],
|
||||
['MSFT', '-1.2%', '378.50', 'HOLD'],
|
||||
['GOOGL', '+0.8%', '141.75', 'BUY']
|
||||
], { headers: ['Symbol', 'Change', 'Price', 'Signal'], title: 'Portfolio' }));
|
||||
|
||||
// Dashboard
|
||||
const dashboard = new Dashboard('Trading Dashboard');
|
||||
dashboard.addEquityCurve(equityData.slice(-50));
|
||||
dashboard.addSignals({
|
||||
AAPL: { direction: 'long', strength: 0.75 },
|
||||
TSLA: { direction: 'short', strength: 0.60 },
|
||||
MSFT: { direction: 'neutral', strength: 0.40 }
|
||||
});
|
||||
console.log(dashboard.render());
|
||||
}
|
||||
Reference in New Issue
Block a user