Squashed 'vendor/ruvector/' content from commit b64c2172

git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
commit d803bfe2b1
7854 changed files with 3522914 additions and 0 deletions

View 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);
});
}

View 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('══════════════════════════════════════════════════════════════════════');
}

View 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('══════════════════════════════════════════════════════════════════════');
}

View 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);
});
}

View 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());
}