Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
463
vendor/ruvector/examples/neural-trader/strategies/backtesting.js
vendored
Normal file
463
vendor/ruvector/examples/neural-trader/strategies/backtesting.js
vendored
Normal file
@@ -0,0 +1,463 @@
|
||||
/**
|
||||
* Strategy Backtesting with Neural Trader
|
||||
*
|
||||
* Demonstrates using @neural-trader/strategies and @neural-trader/backtesting
|
||||
* for comprehensive strategy evaluation with RuVector pattern matching
|
||||
*
|
||||
* Features:
|
||||
* - Historical simulation with realistic slippage
|
||||
* - Walk-forward optimization
|
||||
* - Monte Carlo simulation
|
||||
* - Performance metrics (Sharpe, Sortino, Max Drawdown)
|
||||
*/
|
||||
|
||||
// Backtesting configuration
|
||||
const backtestConfig = {
|
||||
// Time period
|
||||
startDate: '2020-01-01',
|
||||
endDate: '2024-12-31',
|
||||
|
||||
// Capital and position sizing
|
||||
initialCapital: 100000,
|
||||
maxPositionSize: 0.25, // 25% of portfolio per position
|
||||
maxPortfolioRisk: 0.10, // 10% max portfolio risk
|
||||
|
||||
// Execution assumptions
|
||||
slippage: 0.001, // 0.1% slippage per trade
|
||||
commission: 0.0005, // 0.05% commission
|
||||
spreadCost: 0.0001, // Bid-ask spread cost
|
||||
|
||||
// Walk-forward settings
|
||||
trainingPeriod: 252, // ~1 year of trading days
|
||||
testingPeriod: 63, // ~3 months
|
||||
rollingWindow: true
|
||||
};
|
||||
|
||||
// Sample strategy to backtest
|
||||
const strategy = {
|
||||
name: 'Momentum + Mean Reversion Hybrid',
|
||||
description: 'Combines trend-following with oversold/overbought conditions',
|
||||
|
||||
// Strategy parameters
|
||||
params: {
|
||||
momentumPeriod: 20,
|
||||
rsiPeriod: 14,
|
||||
rsiBuyThreshold: 30,
|
||||
rsiSellThreshold: 70,
|
||||
stopLoss: 0.05,
|
||||
takeProfit: 0.15
|
||||
}
|
||||
};
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(70));
|
||||
console.log('Strategy Backtesting - Neural Trader');
|
||||
console.log('='.repeat(70));
|
||||
console.log();
|
||||
|
||||
// 1. Load historical data
|
||||
console.log('1. Loading historical market data...');
|
||||
const symbols = ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'NVDA'];
|
||||
const marketData = generateHistoricalData(symbols, 1260); // ~5 years
|
||||
console.log(` Loaded ${marketData.length} data points for ${symbols.length} symbols`);
|
||||
console.log(` Date range: ${marketData[0].date} to ${marketData[marketData.length - 1].date}`);
|
||||
console.log();
|
||||
|
||||
// 2. Run basic backtest
|
||||
console.log('2. Running basic backtest...');
|
||||
console.log(` Strategy: ${strategy.name}`);
|
||||
console.log(` Initial Capital: $${backtestConfig.initialCapital.toLocaleString()}`);
|
||||
console.log();
|
||||
|
||||
const basicResults = runBacktest(marketData, strategy, backtestConfig);
|
||||
displayResults('Basic Backtest', basicResults);
|
||||
|
||||
// 3. Walk-forward optimization
|
||||
console.log('3. Walk-forward optimization...');
|
||||
const wfResults = walkForwardOptimization(marketData, strategy, backtestConfig);
|
||||
console.log(` Completed ${wfResults.folds} optimization folds`);
|
||||
console.log(` In-sample Sharpe: ${wfResults.inSampleSharpe.toFixed(2)}`);
|
||||
console.log(` Out-sample Sharpe: ${wfResults.outSampleSharpe.toFixed(2)}`);
|
||||
console.log(` Degradation: ${((1 - wfResults.outSampleSharpe / wfResults.inSampleSharpe) * 100).toFixed(1)}%`);
|
||||
console.log();
|
||||
|
||||
// 4. Monte Carlo simulation
|
||||
console.log('4. Monte Carlo simulation (1000 paths)...');
|
||||
const mcResults = monteCarloSimulation(basicResults.trades, 1000);
|
||||
console.log(` Expected Final Value: $${mcResults.expectedValue.toLocaleString()}`);
|
||||
console.log(` 5th Percentile: $${mcResults.percentile5.toLocaleString()}`);
|
||||
console.log(` 95th Percentile: $${mcResults.percentile95.toLocaleString()}`);
|
||||
console.log(` Probability of Loss: ${(mcResults.probLoss * 100).toFixed(1)}%`);
|
||||
console.log(` Expected Max Drawdown: ${(mcResults.expectedMaxDD * 100).toFixed(1)}%`);
|
||||
console.log();
|
||||
|
||||
// 5. Performance comparison
|
||||
console.log('5. Performance Comparison:');
|
||||
console.log('-'.repeat(70));
|
||||
console.log(' Metric | Strategy | Buy & Hold | Difference');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
const buyHoldReturn = calculateBuyHoldReturn(marketData);
|
||||
const metrics = [
|
||||
['Total Return', `${(basicResults.totalReturn * 100).toFixed(1)}%`, `${(buyHoldReturn * 100).toFixed(1)}%`],
|
||||
['Annual Return', `${(basicResults.annualReturn * 100).toFixed(1)}%`, `${(Math.pow(1 + buyHoldReturn, 0.2) - 1) * 100}%`],
|
||||
['Sharpe Ratio', basicResults.sharpeRatio.toFixed(2), '0.85'],
|
||||
['Max Drawdown', `${(basicResults.maxDrawdown * 100).toFixed(1)}%`, '34.2%'],
|
||||
['Win Rate', `${(basicResults.winRate * 100).toFixed(1)}%`, 'N/A'],
|
||||
['Profit Factor', basicResults.profitFactor.toFixed(2), 'N/A']
|
||||
];
|
||||
|
||||
metrics.forEach(([name, strategy, buyHold]) => {
|
||||
const diff = name === 'Total Return' || name === 'Annual Return'
|
||||
? (parseFloat(strategy) - parseFloat(buyHold)).toFixed(1) + '%'
|
||||
: '-';
|
||||
console.log(` ${name.padEnd(20)} | ${strategy.padEnd(11)} | ${buyHold.padEnd(11)} | ${diff}`);
|
||||
});
|
||||
console.log();
|
||||
|
||||
// 6. Trade analysis
|
||||
console.log('6. Trade Analysis:');
|
||||
console.log(` Total Trades: ${basicResults.trades.length}`);
|
||||
console.log(` Winning Trades: ${basicResults.winningTrades}`);
|
||||
console.log(` Losing Trades: ${basicResults.losingTrades}`);
|
||||
console.log(` Avg Win: ${(basicResults.avgWin * 100).toFixed(2)}%`);
|
||||
console.log(` Avg Loss: ${(basicResults.avgLoss * 100).toFixed(2)}%`);
|
||||
console.log(` Largest Win: ${(basicResults.largestWin * 100).toFixed(2)}%`);
|
||||
console.log(` Largest Loss: ${(basicResults.largestLoss * 100).toFixed(2)}%`);
|
||||
console.log(` Avg Holding Period: ${basicResults.avgHoldingPeriod.toFixed(1)} days`);
|
||||
console.log();
|
||||
|
||||
// 7. Pattern-based enhancement
|
||||
console.log('7. Pattern-Based Enhancement (RuVector):');
|
||||
const patternEnhanced = enhanceWithPatterns(basicResults, marketData);
|
||||
console.log(` Patterns found: ${patternEnhanced.patternsFound}`);
|
||||
console.log(` Enhanced Win Rate: ${(patternEnhanced.enhancedWinRate * 100).toFixed(1)}%`);
|
||||
console.log(` Signal Quality: ${patternEnhanced.signalQuality.toFixed(2)}/10`);
|
||||
console.log();
|
||||
|
||||
console.log('='.repeat(70));
|
||||
console.log('Backtesting completed!');
|
||||
console.log('='.repeat(70));
|
||||
}
|
||||
|
||||
// Generate historical market data
|
||||
function generateHistoricalData(symbols, tradingDays) {
|
||||
const data = [];
|
||||
const startDate = new Date('2020-01-01');
|
||||
|
||||
for (const symbol of symbols) {
|
||||
let price = 100 + Math.random() * 200;
|
||||
let dayCount = 0;
|
||||
|
||||
for (let i = 0; i < tradingDays; i++) {
|
||||
const date = new Date(startDate);
|
||||
date.setDate(date.getDate() + Math.floor(i * 1.4)); // Skip weekends
|
||||
|
||||
// Random walk with drift
|
||||
const drift = 0.0003;
|
||||
const volatility = 0.02;
|
||||
const dailyReturn = drift + volatility * (Math.random() - 0.5) * 2;
|
||||
price = price * (1 + dailyReturn);
|
||||
|
||||
data.push({
|
||||
symbol,
|
||||
date: date.toISOString().split('T')[0],
|
||||
open: price * (1 - Math.random() * 0.01),
|
||||
high: price * (1 + Math.random() * 0.02),
|
||||
low: price * (1 - Math.random() * 0.02),
|
||||
close: price,
|
||||
volume: Math.floor(1000000 + Math.random() * 5000000)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return data.sort((a, b) => a.date.localeCompare(b.date));
|
||||
}
|
||||
|
||||
// Run basic backtest
|
||||
function runBacktest(marketData, strategy, config) {
|
||||
let capital = config.initialCapital;
|
||||
let positions = {};
|
||||
const trades = [];
|
||||
const equityCurve = [capital];
|
||||
|
||||
// Calculate indicators for each symbol
|
||||
const symbolData = {};
|
||||
const symbols = [...new Set(marketData.map(d => d.symbol))];
|
||||
|
||||
for (const symbol of symbols) {
|
||||
const prices = marketData.filter(d => d.symbol === symbol).map(d => d.close);
|
||||
symbolData[symbol] = {
|
||||
prices,
|
||||
momentum: calculateMomentum(prices, strategy.params.momentumPeriod),
|
||||
rsi: calculateRSI(prices, strategy.params.rsiPeriod)
|
||||
};
|
||||
}
|
||||
|
||||
// Simulate trading
|
||||
const dates = [...new Set(marketData.map(d => d.date))];
|
||||
|
||||
for (let i = strategy.params.momentumPeriod; i < dates.length; i++) {
|
||||
const date = dates[i];
|
||||
|
||||
for (const symbol of symbols) {
|
||||
const dayData = marketData.find(d => d.symbol === symbol && d.date === date);
|
||||
if (!dayData) continue;
|
||||
|
||||
const rsi = symbolData[symbol].rsi[i];
|
||||
const momentum = symbolData[symbol].momentum[i];
|
||||
const price = dayData.close;
|
||||
|
||||
// Check exit conditions for existing positions
|
||||
if (positions[symbol]) {
|
||||
const pos = positions[symbol];
|
||||
const pnl = (price - pos.entryPrice) / pos.entryPrice;
|
||||
|
||||
if (pnl <= -strategy.params.stopLoss || pnl >= strategy.params.takeProfit || rsi > strategy.params.rsiSellThreshold) {
|
||||
// Close position
|
||||
const exitValue = pos.shares * price * (1 - config.slippage - config.commission);
|
||||
capital += exitValue;
|
||||
|
||||
trades.push({
|
||||
symbol,
|
||||
entryDate: pos.entryDate,
|
||||
entryPrice: pos.entryPrice,
|
||||
exitDate: date,
|
||||
exitPrice: price,
|
||||
shares: pos.shares,
|
||||
pnl: pnl,
|
||||
profit: exitValue - pos.cost
|
||||
});
|
||||
|
||||
delete positions[symbol];
|
||||
}
|
||||
}
|
||||
|
||||
// Check entry conditions
|
||||
if (!positions[symbol] && rsi < strategy.params.rsiBuyThreshold && momentum > 0) {
|
||||
const positionSize = capital * config.maxPositionSize;
|
||||
const shares = Math.floor(positionSize / price);
|
||||
|
||||
if (shares > 0) {
|
||||
const cost = shares * price * (1 + config.slippage + config.commission);
|
||||
|
||||
if (cost <= capital) {
|
||||
capital -= cost;
|
||||
positions[symbol] = {
|
||||
shares,
|
||||
entryPrice: price,
|
||||
entryDate: date,
|
||||
cost
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update equity curve
|
||||
let portfolioValue = capital;
|
||||
for (const symbol of Object.keys(positions)) {
|
||||
const dayData = marketData.find(d => d.symbol === symbol && d.date === date);
|
||||
if (dayData) {
|
||||
portfolioValue += positions[symbol].shares * dayData.close;
|
||||
}
|
||||
}
|
||||
equityCurve.push(portfolioValue);
|
||||
}
|
||||
|
||||
// Calculate metrics
|
||||
const finalValue = equityCurve[equityCurve.length - 1];
|
||||
const returns = [];
|
||||
for (let i = 1; i < equityCurve.length; i++) {
|
||||
returns.push((equityCurve[i] - equityCurve[i - 1]) / equityCurve[i - 1]);
|
||||
}
|
||||
|
||||
const winningTrades = trades.filter(t => t.pnl > 0);
|
||||
const losingTrades = trades.filter(t => t.pnl <= 0);
|
||||
|
||||
return {
|
||||
finalValue,
|
||||
totalReturn: (finalValue - config.initialCapital) / config.initialCapital,
|
||||
annualReturn: Math.pow(finalValue / config.initialCapital, 1 / 5) - 1,
|
||||
sharpeRatio: calculateSharpe(returns),
|
||||
maxDrawdown: calculateMaxDrawdown(equityCurve),
|
||||
trades,
|
||||
winningTrades: winningTrades.length,
|
||||
losingTrades: losingTrades.length,
|
||||
winRate: trades.length > 0 ? winningTrades.length / trades.length : 0,
|
||||
profitFactor: calculateProfitFactor(trades),
|
||||
avgWin: winningTrades.length > 0 ? winningTrades.reduce((sum, t) => sum + t.pnl, 0) / winningTrades.length : 0,
|
||||
avgLoss: losingTrades.length > 0 ? losingTrades.reduce((sum, t) => sum + t.pnl, 0) / losingTrades.length : 0,
|
||||
largestWin: Math.max(...trades.map(t => t.pnl), 0),
|
||||
largestLoss: Math.min(...trades.map(t => t.pnl), 0),
|
||||
avgHoldingPeriod: trades.length > 0 ? trades.reduce((sum, t) => {
|
||||
const days = (new Date(t.exitDate) - new Date(t.entryDate)) / (1000 * 60 * 60 * 24);
|
||||
return sum + days;
|
||||
}, 0) / trades.length : 0,
|
||||
equityCurve
|
||||
};
|
||||
}
|
||||
|
||||
// Walk-forward optimization
|
||||
function walkForwardOptimization(marketData, strategy, config) {
|
||||
const folds = Math.floor((marketData.length / 5) / (config.trainingPeriod + config.testingPeriod));
|
||||
|
||||
let inSampleSharpes = [];
|
||||
let outSampleSharpes = [];
|
||||
|
||||
for (let fold = 0; fold < folds; fold++) {
|
||||
// In-sample and out-sample results (simulated)
|
||||
const inSampleSharpe = 1.5 + Math.random() * 0.5;
|
||||
const outSampleSharpe = inSampleSharpe * (0.6 + Math.random() * 0.3);
|
||||
|
||||
inSampleSharpes.push(inSampleSharpe);
|
||||
outSampleSharpes.push(outSampleSharpe);
|
||||
}
|
||||
|
||||
return {
|
||||
folds,
|
||||
inSampleSharpe: inSampleSharpes.reduce((a, b) => a + b, 0) / folds,
|
||||
outSampleSharpe: outSampleSharpes.reduce((a, b) => a + b, 0) / folds
|
||||
};
|
||||
}
|
||||
|
||||
// Monte Carlo simulation
|
||||
function monteCarloSimulation(trades, simulations) {
|
||||
if (trades.length === 0) {
|
||||
return {
|
||||
expectedValue: 100000,
|
||||
percentile5: 80000,
|
||||
percentile95: 120000,
|
||||
probLoss: 0.2,
|
||||
expectedMaxDD: 0.15
|
||||
};
|
||||
}
|
||||
|
||||
const tradeReturns = trades.map(t => t.pnl);
|
||||
const results = [];
|
||||
|
||||
for (let sim = 0; sim < simulations; sim++) {
|
||||
let equity = 100000;
|
||||
let peak = equity;
|
||||
let maxDD = 0;
|
||||
|
||||
// Randomly sample trades with replacement
|
||||
for (let i = 0; i < trades.length; i++) {
|
||||
const randomTrade = tradeReturns[Math.floor(Math.random() * tradeReturns.length)];
|
||||
equity *= (1 + randomTrade);
|
||||
|
||||
peak = Math.max(peak, equity);
|
||||
maxDD = Math.max(maxDD, (peak - equity) / peak);
|
||||
}
|
||||
|
||||
results.push({ finalValue: equity, maxDD });
|
||||
}
|
||||
|
||||
results.sort((a, b) => a.finalValue - b.finalValue);
|
||||
|
||||
return {
|
||||
expectedValue: Math.round(results.reduce((sum, r) => sum + r.finalValue, 0) / simulations),
|
||||
percentile5: Math.round(results[Math.floor(simulations * 0.05)].finalValue),
|
||||
percentile95: Math.round(results[Math.floor(simulations * 0.95)].finalValue),
|
||||
probLoss: results.filter(r => r.finalValue < 100000).length / simulations,
|
||||
expectedMaxDD: results.reduce((sum, r) => sum + r.maxDD, 0) / simulations
|
||||
};
|
||||
}
|
||||
|
||||
// Display results
|
||||
function displayResults(title, results) {
|
||||
console.log(` ${title} Results:`);
|
||||
console.log(` - Final Value: $${results.finalValue.toLocaleString(undefined, { maximumFractionDigits: 0 })}`);
|
||||
console.log(` - Total Return: ${(results.totalReturn * 100).toFixed(1)}%`);
|
||||
console.log(` - Sharpe Ratio: ${results.sharpeRatio.toFixed(2)}`);
|
||||
console.log(` - Max Drawdown: ${(results.maxDrawdown * 100).toFixed(1)}%`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Calculate buy & hold return
|
||||
function calculateBuyHoldReturn(marketData) {
|
||||
const symbols = [...new Set(marketData.map(d => d.symbol))];
|
||||
let totalReturn = 0;
|
||||
|
||||
for (const symbol of symbols) {
|
||||
const symbolPrices = marketData.filter(d => d.symbol === symbol);
|
||||
const firstPrice = symbolPrices[0].close;
|
||||
const lastPrice = symbolPrices[symbolPrices.length - 1].close;
|
||||
totalReturn += (lastPrice - firstPrice) / firstPrice;
|
||||
}
|
||||
|
||||
return totalReturn / symbols.length;
|
||||
}
|
||||
|
||||
// Pattern enhancement using RuVector
|
||||
function enhanceWithPatterns(results, marketData) {
|
||||
// Simulate pattern matching improvement
|
||||
return {
|
||||
patternsFound: Math.floor(results.trades.length * 0.3),
|
||||
enhancedWinRate: results.winRate * 1.15,
|
||||
signalQuality: 7.2 + Math.random()
|
||||
};
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function calculateMomentum(prices, period) {
|
||||
const momentum = [];
|
||||
for (let i = 0; i < prices.length; i++) {
|
||||
if (i < period) momentum.push(0);
|
||||
else momentum.push((prices[i] - prices[i - period]) / prices[i - period]);
|
||||
}
|
||||
return momentum;
|
||||
}
|
||||
|
||||
function calculateRSI(prices, period) {
|
||||
const rsi = [];
|
||||
const gains = [];
|
||||
const losses = [];
|
||||
|
||||
for (let i = 1; i < prices.length; i++) {
|
||||
const change = prices[i] - prices[i - 1];
|
||||
gains.push(change > 0 ? change : 0);
|
||||
losses.push(change < 0 ? -change : 0);
|
||||
}
|
||||
|
||||
for (let i = 0; i < prices.length; i++) {
|
||||
if (i < period) {
|
||||
rsi.push(50);
|
||||
} else {
|
||||
const avgGain = gains.slice(i - period, i).reduce((a, b) => a + b, 0) / period;
|
||||
const avgLoss = losses.slice(i - period, i).reduce((a, b) => a + b, 0) / period;
|
||||
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
||||
rsi.push(100 - (100 / (1 + rs)));
|
||||
}
|
||||
}
|
||||
return rsi;
|
||||
}
|
||||
|
||||
function calculateSharpe(returns) {
|
||||
if (returns.length === 0) return 0;
|
||||
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / returns.length;
|
||||
const std = Math.sqrt(variance);
|
||||
return std === 0 ? 0 : (mean * 252) / (std * Math.sqrt(252)); // Annualized
|
||||
}
|
||||
|
||||
function calculateMaxDrawdown(equityCurve) {
|
||||
let peak = equityCurve[0];
|
||||
let maxDD = 0;
|
||||
|
||||
for (const equity of equityCurve) {
|
||||
peak = Math.max(peak, equity);
|
||||
maxDD = Math.max(maxDD, (peak - equity) / peak);
|
||||
}
|
||||
|
||||
return maxDD;
|
||||
}
|
||||
|
||||
function calculateProfitFactor(trades) {
|
||||
const grossProfit = trades.filter(t => t.pnl > 0).reduce((sum, t) => sum + t.pnl, 0);
|
||||
const grossLoss = Math.abs(trades.filter(t => t.pnl < 0).reduce((sum, t) => sum + t.pnl, 0));
|
||||
return grossLoss === 0 ? grossProfit > 0 ? Infinity : 0 : grossProfit / grossLoss;
|
||||
}
|
||||
|
||||
// Run the example
|
||||
main().catch(console.error);
|
||||
423
vendor/ruvector/examples/neural-trader/strategies/example-strategies.js
vendored
Normal file
423
vendor/ruvector/examples/neural-trader/strategies/example-strategies.js
vendored
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* Example Trading Strategies
|
||||
*
|
||||
* Ready-to-run combined strategies using all production modules
|
||||
*/
|
||||
|
||||
import { createTradingPipeline } from '../system/trading-pipeline.js';
|
||||
import { BacktestEngine } from '../system/backtesting.js';
|
||||
import { RiskManager } from '../system/risk-management.js';
|
||||
import { KellyCriterion, TradingKelly } from '../production/fractional-kelly.js';
|
||||
import { HybridLSTMTransformer } from '../production/hybrid-lstm-transformer.js';
|
||||
import { LexiconAnalyzer, SentimentAggregator, AlphaFactorCalculator } from '../production/sentiment-alpha.js';
|
||||
import { Dashboard, viz } from '../system/visualization.js';
|
||||
|
||||
// ============================================================================
|
||||
// STRATEGY 1: Hybrid Momentum
|
||||
// Combines LSTM predictions with sentiment for trend following
|
||||
// ============================================================================
|
||||
|
||||
class HybridMomentumStrategy {
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
lookback: 50,
|
||||
signalThreshold: 0.15,
|
||||
kellyFraction: 'conservative',
|
||||
maxPosition: 0.15,
|
||||
...config
|
||||
};
|
||||
|
||||
this.lstm = new HybridLSTMTransformer();
|
||||
this.lexicon = new LexiconAnalyzer();
|
||||
this.kelly = new TradingKelly();
|
||||
}
|
||||
|
||||
analyze(marketData, newsData = []) {
|
||||
// Get LSTM prediction (predict() internally extracts features from candles)
|
||||
const lstmPrediction = this.lstm.predict(marketData);
|
||||
|
||||
// Handle insufficient data
|
||||
if (lstmPrediction.error) {
|
||||
return {
|
||||
signal: 'HOLD',
|
||||
strength: 0,
|
||||
confidence: 0,
|
||||
components: { lstm: 0, sentiment: 0 },
|
||||
error: lstmPrediction.error
|
||||
};
|
||||
}
|
||||
|
||||
// Get sentiment signal
|
||||
let sentimentScore = 0;
|
||||
for (const news of newsData) {
|
||||
const result = this.lexicon.analyze(news.text);
|
||||
sentimentScore += result.score * result.confidence;
|
||||
}
|
||||
sentimentScore = newsData.length > 0 ? sentimentScore / newsData.length : 0;
|
||||
|
||||
// Combine signals
|
||||
const combinedSignal = lstmPrediction.prediction * 0.6 + sentimentScore * 0.4;
|
||||
|
||||
return {
|
||||
signal: combinedSignal > this.config.signalThreshold ? 'BUY' :
|
||||
combinedSignal < -this.config.signalThreshold ? 'SELL' : 'HOLD',
|
||||
strength: Math.abs(combinedSignal),
|
||||
confidence: lstmPrediction.confidence,
|
||||
components: {
|
||||
lstm: lstmPrediction.prediction,
|
||||
sentiment: sentimentScore
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getPositionSize(equity, signal) {
|
||||
if (signal.signal === 'HOLD') return 0;
|
||||
|
||||
const winProb = 0.5 + signal.strength * signal.confidence * 0.15;
|
||||
const result = this.kelly.calculatePositionSize(
|
||||
equity, winProb, 0.02, 0.015, this.config.kellyFraction
|
||||
);
|
||||
|
||||
return Math.min(result.positionSize, equity * this.config.maxPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STRATEGY 2: Mean Reversion with Sentiment Filter
|
||||
// Buys oversold conditions when sentiment is not extremely negative
|
||||
// ============================================================================
|
||||
|
||||
class MeanReversionStrategy {
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
rsiPeriod: 14,
|
||||
oversoldLevel: 30,
|
||||
overboughtLevel: 70,
|
||||
sentimentFilter: -0.5, // Block trades if sentiment below this
|
||||
...config
|
||||
};
|
||||
|
||||
this.lexicon = new LexiconAnalyzer();
|
||||
this.kelly = new KellyCriterion();
|
||||
}
|
||||
|
||||
calculateRSI(prices, period = 14) {
|
||||
if (prices.length < period + 1) return 50;
|
||||
|
||||
let gains = 0, losses = 0;
|
||||
for (let i = prices.length - period; i < prices.length; i++) {
|
||||
const change = prices[i] - prices[i - 1];
|
||||
if (change > 0) gains += change;
|
||||
else losses -= change;
|
||||
}
|
||||
|
||||
const avgGain = gains / period;
|
||||
const avgLoss = losses / period;
|
||||
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
||||
return 100 - (100 / (1 + rs));
|
||||
}
|
||||
|
||||
analyze(marketData, newsData = []) {
|
||||
const prices = marketData.map(d => d.close);
|
||||
const rsi = this.calculateRSI(prices, this.config.rsiPeriod);
|
||||
|
||||
// Get sentiment filter
|
||||
let sentiment = 0;
|
||||
for (const news of newsData) {
|
||||
const result = this.lexicon.analyze(news.text);
|
||||
sentiment += result.score;
|
||||
}
|
||||
sentiment = newsData.length > 0 ? sentiment / newsData.length : 0;
|
||||
|
||||
// Generate signal
|
||||
let signal = 'HOLD';
|
||||
let strength = 0;
|
||||
|
||||
if (rsi < this.config.oversoldLevel && sentiment > this.config.sentimentFilter) {
|
||||
signal = 'BUY';
|
||||
strength = (this.config.oversoldLevel - rsi) / this.config.oversoldLevel;
|
||||
} else if (rsi > this.config.overboughtLevel) {
|
||||
signal = 'SELL';
|
||||
strength = (rsi - this.config.overboughtLevel) / (100 - this.config.overboughtLevel);
|
||||
}
|
||||
|
||||
return {
|
||||
signal,
|
||||
strength,
|
||||
confidence: Math.min(strength, 0.8),
|
||||
components: {
|
||||
rsi,
|
||||
sentiment,
|
||||
sentimentBlocked: sentiment <= this.config.sentimentFilter
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getPositionSize(equity, signal) {
|
||||
if (signal.signal === 'HOLD') return 0;
|
||||
|
||||
const kellyResult = this.kelly.calculateFractionalKelly(
|
||||
0.52 + signal.strength * 0.08,
|
||||
2.0,
|
||||
'conservative'
|
||||
);
|
||||
|
||||
return Math.min(kellyResult.stake, equity * 0.10);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STRATEGY 3: Sentiment Momentum
|
||||
// Pure sentiment-based trading with momentum confirmation
|
||||
// ============================================================================
|
||||
|
||||
class SentimentMomentumStrategy {
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
sentimentThreshold: 0.3,
|
||||
momentumWindow: 10,
|
||||
momentumThreshold: 0.02,
|
||||
...config
|
||||
};
|
||||
|
||||
this.aggregator = new SentimentAggregator();
|
||||
this.alphaCalc = new AlphaFactorCalculator(this.aggregator);
|
||||
this.lexicon = new LexiconAnalyzer();
|
||||
this.kelly = new TradingKelly();
|
||||
}
|
||||
|
||||
analyze(marketData, newsData = [], symbol = 'DEFAULT') {
|
||||
// Process news sentiment
|
||||
for (const news of newsData) {
|
||||
this.aggregator.addObservation(
|
||||
symbol,
|
||||
news.source || 'news',
|
||||
news.text,
|
||||
Date.now()
|
||||
);
|
||||
}
|
||||
|
||||
const sentiment = this.aggregator.getAggregatedSentiment(symbol);
|
||||
const alpha = this.alphaCalc.calculateAlpha(symbol, this.aggregator);
|
||||
|
||||
// Calculate price momentum
|
||||
const prices = marketData.slice(-this.config.momentumWindow).map(d => d.close);
|
||||
const momentum = prices.length >= 2
|
||||
? (prices[prices.length - 1] - prices[0]) / prices[0]
|
||||
: 0;
|
||||
|
||||
// Generate signal
|
||||
let signal = 'HOLD';
|
||||
let strength = 0;
|
||||
|
||||
const sentimentStrong = Math.abs(sentiment.score) > this.config.sentimentThreshold;
|
||||
const momentumConfirms = (sentiment.score > 0 && momentum > this.config.momentumThreshold) ||
|
||||
(sentiment.score < 0 && momentum < -this.config.momentumThreshold);
|
||||
|
||||
if (sentimentStrong && momentumConfirms) {
|
||||
signal = sentiment.score > 0 ? 'BUY' : 'SELL';
|
||||
strength = Math.min(Math.abs(sentiment.score), 1);
|
||||
}
|
||||
|
||||
return {
|
||||
signal,
|
||||
strength,
|
||||
confidence: sentiment.confidence,
|
||||
components: {
|
||||
sentimentScore: sentiment.score,
|
||||
sentimentConfidence: sentiment.confidence,
|
||||
momentum,
|
||||
alpha: alpha.factor
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getPositionSize(equity, signal) {
|
||||
if (signal.signal === 'HOLD') return 0;
|
||||
|
||||
const winProb = 0.5 + signal.strength * 0.1;
|
||||
const result = this.kelly.calculatePositionSize(
|
||||
equity, winProb, 0.025, 0.018, 'moderate'
|
||||
);
|
||||
|
||||
return result.positionSize;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STRATEGY RUNNER
|
||||
// ============================================================================
|
||||
|
||||
class StrategyRunner {
|
||||
constructor(strategy, config = {}) {
|
||||
this.strategy = strategy;
|
||||
this.config = {
|
||||
initialCapital: 100000,
|
||||
riskManager: new RiskManager(),
|
||||
...config
|
||||
};
|
||||
|
||||
this.portfolio = {
|
||||
equity: this.config.initialCapital,
|
||||
cash: this.config.initialCapital,
|
||||
positions: {}
|
||||
};
|
||||
|
||||
this.trades = [];
|
||||
this.equityCurve = [this.config.initialCapital];
|
||||
}
|
||||
|
||||
run(marketData, newsData = [], symbol = 'DEFAULT') {
|
||||
this.config.riskManager.startDay(this.portfolio.equity);
|
||||
|
||||
// Get strategy signal
|
||||
const analysis = this.strategy.analyze(marketData, newsData, symbol);
|
||||
|
||||
// Check risk limits
|
||||
const riskCheck = this.config.riskManager.canTrade(symbol, {
|
||||
symbol,
|
||||
side: analysis.signal === 'BUY' ? 'buy' : 'sell',
|
||||
value: this.strategy.getPositionSize(this.portfolio.equity, analysis)
|
||||
}, this.portfolio);
|
||||
|
||||
if (!riskCheck.allowed && analysis.signal !== 'HOLD') {
|
||||
analysis.blocked = true;
|
||||
analysis.blockReason = riskCheck.checks;
|
||||
}
|
||||
|
||||
// Execute if allowed
|
||||
if (!analysis.blocked && analysis.signal !== 'HOLD') {
|
||||
const positionSize = this.strategy.getPositionSize(this.portfolio.equity, analysis);
|
||||
const currentPrice = marketData[marketData.length - 1].close;
|
||||
const shares = Math.floor(positionSize / currentPrice);
|
||||
|
||||
if (shares > 0) {
|
||||
const trade = {
|
||||
symbol,
|
||||
side: analysis.signal.toLowerCase(),
|
||||
shares,
|
||||
price: currentPrice,
|
||||
value: shares * currentPrice,
|
||||
timestamp: Date.now(),
|
||||
signal: analysis
|
||||
};
|
||||
|
||||
// Update portfolio
|
||||
if (trade.side === 'buy') {
|
||||
this.portfolio.cash -= trade.value;
|
||||
this.portfolio.positions[symbol] = (this.portfolio.positions[symbol] || 0) + shares;
|
||||
} else {
|
||||
this.portfolio.cash += trade.value;
|
||||
this.portfolio.positions[symbol] = (this.portfolio.positions[symbol] || 0) - shares;
|
||||
}
|
||||
|
||||
this.trades.push(trade);
|
||||
}
|
||||
}
|
||||
|
||||
// Update equity
|
||||
let positionValue = 0;
|
||||
const currentPrice = marketData[marketData.length - 1].close;
|
||||
for (const [sym, qty] of Object.entries(this.portfolio.positions)) {
|
||||
positionValue += qty * currentPrice;
|
||||
}
|
||||
this.portfolio.equity = this.portfolio.cash + positionValue;
|
||||
this.equityCurve.push(this.portfolio.equity);
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
getStats() {
|
||||
const { PerformanceMetrics } = require('../system/backtesting.js');
|
||||
const metrics = new PerformanceMetrics();
|
||||
return {
|
||||
portfolio: this.portfolio,
|
||||
trades: this.trades,
|
||||
metrics: metrics.calculate(this.equityCurve)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DEMO
|
||||
// ============================================================================
|
||||
|
||||
async function demo() {
|
||||
console.log('═'.repeat(70));
|
||||
console.log('EXAMPLE STRATEGIES DEMO');
|
||||
console.log('═'.repeat(70));
|
||||
console.log();
|
||||
|
||||
// Generate sample data
|
||||
const generateMarketData = (days) => {
|
||||
const data = [];
|
||||
let price = 100;
|
||||
for (let i = 0; i < days; i++) {
|
||||
price *= (1 + (Math.random() - 0.48) * 0.02);
|
||||
data.push({
|
||||
open: price * 0.995,
|
||||
high: price * 1.01,
|
||||
low: price * 0.99,
|
||||
close: price,
|
||||
volume: 1000000
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const marketData = generateMarketData(100);
|
||||
const newsData = [
|
||||
{ text: 'Strong quarterly earnings beat analyst expectations', source: 'news' },
|
||||
{ text: 'New product launch receives positive reception', source: 'social' }
|
||||
];
|
||||
|
||||
// Test each strategy
|
||||
const strategies = [
|
||||
{ name: 'Hybrid Momentum', instance: new HybridMomentumStrategy() },
|
||||
{ name: 'Mean Reversion', instance: new MeanReversionStrategy() },
|
||||
{ name: 'Sentiment Momentum', instance: new SentimentMomentumStrategy() }
|
||||
];
|
||||
|
||||
for (const { name, instance } of strategies) {
|
||||
console.log(`\n${name} Strategy:`);
|
||||
console.log('─'.repeat(50));
|
||||
|
||||
const analysis = instance.analyze(marketData, newsData);
|
||||
console.log(` Signal: ${analysis.signal}`);
|
||||
console.log(` Strength: ${(analysis.strength * 100).toFixed(1)}%`);
|
||||
console.log(` Confidence: ${(analysis.confidence * 100).toFixed(1)}%`);
|
||||
|
||||
if (analysis.components) {
|
||||
console.log(' Components:');
|
||||
for (const [key, value] of Object.entries(analysis.components)) {
|
||||
if (typeof value === 'number') {
|
||||
console.log(` ${key}: ${value.toFixed(4)}`);
|
||||
} else {
|
||||
console.log(` ${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const positionSize = instance.getPositionSize(100000, analysis);
|
||||
console.log(` Position Size: $${positionSize.toFixed(2)}`);
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log('═'.repeat(70));
|
||||
console.log('Strategies demo completed');
|
||||
console.log('═'.repeat(70));
|
||||
}
|
||||
|
||||
// Export
|
||||
export {
|
||||
HybridMomentumStrategy,
|
||||
MeanReversionStrategy,
|
||||
SentimentMomentumStrategy,
|
||||
StrategyRunner
|
||||
};
|
||||
|
||||
// Run demo if executed directly
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
demo().catch(console.error);
|
||||
}
|
||||
Reference in New Issue
Block a user