Files
wifi-densepose/examples/neural-trader/production/sentiment-alpha.js
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

723 lines
22 KiB
JavaScript

/**
* Sentiment Alpha Pipeline
*
* PRODUCTION: LLM-based sentiment analysis for trading alpha generation
*
* Research basis:
* - 3% annual excess returns from sentiment (2024)
* - 50.63% return over 28 months (backtested)
* - FinBERT embeddings outperform technical signals
*
* Features:
* - Multi-source sentiment aggregation (news, social, earnings)
* - Sentiment scoring and signal generation
* - Calibration for trading decisions
* - Integration with Kelly criterion for sizing
*/
// Sentiment Configuration
const sentimentConfig = {
// Source weights
sources: {
news: { weight: 0.40, decay: 0.95 }, // News articles
social: { weight: 0.25, decay: 0.90 }, // Social media
earnings: { weight: 0.25, decay: 0.99 }, // Earnings calls
analyst: { weight: 0.10, decay: 0.98 } // Analyst reports
},
// Sentiment thresholds
thresholds: {
strongBullish: 0.6,
bullish: 0.3,
neutral: [-0.1, 0.1],
bearish: -0.3,
strongBearish: -0.6
},
// Signal generation
signals: {
minConfidence: 0.6,
lookbackDays: 7,
smoothingWindow: 3,
contrarianThreshold: 0.8 // Extreme sentiment = contrarian signal
},
// Alpha calibration
calibration: {
historicalAccuracy: 0.55, // Historical prediction accuracy
shrinkageFactor: 0.3 // Shrink extreme predictions
}
};
/**
* Lexicon-based Sentiment Analyzer
* Fast, interpretable sentiment scoring
*/
class LexiconAnalyzer {
constructor() {
// Financial sentiment lexicon (simplified)
this.positiveWords = new Set([
'growth', 'profit', 'gains', 'bullish', 'upgrade', 'beat', 'exceeded',
'outperform', 'strong', 'surge', 'rally', 'breakthrough', 'innovation',
'record', 'momentum', 'optimistic', 'recovery', 'expansion', 'success',
'opportunity', 'positive', 'increase', 'improve', 'advance', 'boost'
]);
this.negativeWords = new Set([
'loss', 'decline', 'bearish', 'downgrade', 'miss', 'below', 'weak',
'underperform', 'crash', 'plunge', 'risk', 'concern', 'warning',
'recession', 'inflation', 'uncertainty', 'volatility', 'default',
'bankruptcy', 'negative', 'decrease', 'drop', 'fall', 'cut', 'layoff'
]);
this.intensifiers = new Set([
'very', 'extremely', 'significantly', 'strongly', 'substantially',
'dramatically', 'sharply', 'massive', 'huge', 'major'
]);
this.negators = new Set([
'not', 'no', 'never', 'neither', 'without', 'hardly', 'barely'
]);
}
// Optimized analyze (avoids regex, minimizes allocations)
analyze(text) {
const lowerText = text.toLowerCase();
let score = 0;
let positiveCount = 0;
let negativeCount = 0;
let intensifierActive = false;
let negatorActive = false;
let wordCount = 0;
// Extract words without regex (faster)
let wordStart = -1;
const len = lowerText.length;
for (let i = 0; i <= len; i++) {
const c = i < len ? lowerText.charCodeAt(i) : 32; // Space at end
const isWordChar = (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95; // a-z, 0-9, _
if (isWordChar && wordStart === -1) {
wordStart = i;
} else if (!isWordChar && wordStart !== -1) {
const word = lowerText.slice(wordStart, i);
wordStart = -1;
wordCount++;
// Check for intensifiers and negators
if (this.intensifiers.has(word)) {
intensifierActive = true;
continue;
}
if (this.negators.has(word)) {
negatorActive = true;
continue;
}
// Score sentiment words
let wordScore = 0;
if (this.positiveWords.has(word)) {
wordScore = 1;
positiveCount++;
} else if (this.negativeWords.has(word)) {
wordScore = -1;
negativeCount++;
}
// Apply modifiers
if (wordScore !== 0) {
if (intensifierActive) wordScore *= 1.5;
if (negatorActive) wordScore *= -1;
score += wordScore;
}
// Reset modifiers
intensifierActive = false;
negatorActive = false;
}
}
// Normalize score
const totalSentimentWords = positiveCount + negativeCount;
const normalizedScore = totalSentimentWords > 0
? score / (totalSentimentWords * 1.5)
: 0;
return {
score: Math.max(-1, Math.min(1, normalizedScore)),
positiveCount,
negativeCount,
totalWords: wordCount,
confidence: Math.min(1, totalSentimentWords / 10)
};
}
}
/**
* Embedding-based Sentiment Analyzer
* Simulates FinBERT-style deep learning analysis
*/
class EmbeddingAnalyzer {
constructor() {
// Simulated embedding weights (in production, use actual model)
this.embeddingDim = 64;
this.sentimentProjection = Array(this.embeddingDim).fill(null)
.map(() => (Math.random() - 0.5) * 0.1);
}
// Simulate text embedding
embed(text) {
const words = text.toLowerCase().split(/\s+/);
const embedding = new Array(this.embeddingDim).fill(0);
// Simple hash-based embedding simulation
for (const word of words) {
const hash = this.hashString(word);
for (let i = 0; i < this.embeddingDim; i++) {
embedding[i] += Math.sin(hash * (i + 1)) / words.length;
}
}
return embedding;
}
hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash = hash & hash;
}
return hash;
}
analyze(text) {
const embedding = this.embed(text);
// Project to sentiment score
let score = 0;
for (let i = 0; i < this.embeddingDim; i++) {
score += embedding[i] * this.sentimentProjection[i];
}
// Normalize
score = Math.tanh(score * 10);
return {
score,
embedding: embedding.slice(0, 8), // Return first 8 dims
confidence: Math.abs(score)
};
}
}
/**
* Sentiment Source Aggregator
* Combines multiple sentiment sources with decay
*/
class SentimentAggregator {
constructor(config = sentimentConfig) {
this.config = config;
this.lexiconAnalyzer = new LexiconAnalyzer();
this.embeddingAnalyzer = new EmbeddingAnalyzer();
this.sentimentHistory = new Map(); // symbol -> sentiment history
}
// Add sentiment observation
addObservation(symbol, source, text, timestamp = Date.now()) {
if (!this.sentimentHistory.has(symbol)) {
this.sentimentHistory.set(symbol, []);
}
// Analyze with both methods
const lexicon = this.lexiconAnalyzer.analyze(text);
const embedding = this.embeddingAnalyzer.analyze(text);
// Combine scores
const combinedScore = 0.4 * lexicon.score + 0.6 * embedding.score;
const combinedConfidence = Math.sqrt(lexicon.confidence * embedding.confidence);
const observation = {
timestamp,
source,
score: combinedScore,
confidence: combinedConfidence,
lexiconScore: lexicon.score,
embeddingScore: embedding.score,
text: text.substring(0, 100)
};
this.sentimentHistory.get(symbol).push(observation);
// Limit history size
const history = this.sentimentHistory.get(symbol);
if (history.length > 1000) {
history.splice(0, history.length - 1000);
}
return observation;
}
// Get aggregated sentiment for symbol
getAggregatedSentiment(symbol, lookbackMs = 7 * 24 * 60 * 60 * 1000) {
const history = this.sentimentHistory.get(symbol);
if (!history || history.length === 0) {
return { score: 0, confidence: 0, count: 0 };
}
const cutoff = Date.now() - lookbackMs;
const recent = history.filter(h => h.timestamp >= cutoff);
if (recent.length === 0) {
return { score: 0, confidence: 0, count: 0 };
}
// Weight by source, recency, and confidence
let weightedSum = 0;
let totalWeight = 0;
const sourceCounts = {};
for (const obs of recent) {
const sourceConfig = this.config.sources[obs.source] || { weight: 0.25, decay: 0.95 };
const age = (Date.now() - obs.timestamp) / (24 * 60 * 60 * 1000); // days
const decayFactor = Math.pow(sourceConfig.decay, age);
const weight = sourceConfig.weight * decayFactor * obs.confidence;
weightedSum += obs.score * weight;
totalWeight += weight;
sourceCounts[obs.source] = (sourceCounts[obs.source] || 0) + 1;
}
const aggregatedScore = totalWeight > 0 ? weightedSum / totalWeight : 0;
const confidence = Math.min(1, totalWeight / 2); // Confidence based on weight
return {
score: aggregatedScore,
confidence,
count: recent.length,
sourceCounts,
dominant: Object.entries(sourceCounts).sort((a, b) => b[1] - a[1])[0]?.[0]
};
}
// Generate trading signal
generateSignal(symbol) {
const sentiment = this.getAggregatedSentiment(symbol);
if (sentiment.confidence < this.config.signals.minConfidence) {
return {
signal: 'HOLD',
reason: 'low_confidence',
sentiment
};
}
// Check for contrarian opportunity (extreme sentiment)
if (Math.abs(sentiment.score) >= this.config.signals.contrarianThreshold) {
return {
signal: sentiment.score > 0 ? 'CONTRARIAN_SELL' : 'CONTRARIAN_BUY',
reason: 'extreme_sentiment',
sentiment,
warning: 'Contrarian signal - high risk'
};
}
// Standard signals
const thresholds = this.config.thresholds;
let signal, strength;
if (sentiment.score >= thresholds.strongBullish) {
signal = 'STRONG_BUY';
strength = 'high';
} else if (sentiment.score >= thresholds.bullish) {
signal = 'BUY';
strength = 'medium';
} else if (sentiment.score <= thresholds.strongBearish) {
signal = 'STRONG_SELL';
strength = 'high';
} else if (sentiment.score <= thresholds.bearish) {
signal = 'SELL';
strength = 'medium';
} else {
signal = 'HOLD';
strength = 'low';
}
return {
signal,
strength,
sentiment,
calibratedProbability: this.calibrateProbability(sentiment.score)
};
}
// Calibrate sentiment to win probability
calibrateProbability(sentimentScore) {
// Map sentiment [-1, 1] to probability [0.3, 0.7]
// Apply shrinkage toward 0.5
const rawProb = 0.5 + sentimentScore * 0.2;
const shrinkage = this.config.calibration.shrinkageFactor;
const calibrated = rawProb * (1 - shrinkage) + 0.5 * shrinkage;
return Math.max(0.3, Math.min(0.7, calibrated));
}
}
/**
* News Sentiment Stream Processor
* Processes incoming news for real-time sentiment
*/
class NewsSentimentStream {
constructor(config = sentimentConfig) {
this.aggregator = new SentimentAggregator(config);
this.alerts = [];
}
// Process news item
processNews(item) {
const { symbol, headline, source, timestamp } = item;
const observation = this.aggregator.addObservation(
symbol,
source || 'news',
headline,
timestamp || Date.now()
);
// Check for significant sentiment
if (Math.abs(observation.score) >= 0.5 && observation.confidence >= 0.6) {
this.alerts.push({
timestamp: Date.now(),
symbol,
score: observation.score,
headline: headline.substring(0, 80)
});
}
return observation;
}
// Process batch of news
processBatch(items) {
return items.map(item => this.processNews(item));
}
// Get signals for all tracked symbols
getAllSignals() {
const signals = {};
for (const symbol of this.aggregator.sentimentHistory.keys()) {
signals[symbol] = this.aggregator.generateSignal(symbol);
}
return signals;
}
// Get recent alerts
getAlerts(limit = 10) {
return this.alerts.slice(-limit);
}
}
/**
* Alpha Factor Calculator
* Converts sentiment to tradeable alpha factors
*/
class AlphaFactorCalculator {
constructor(config = sentimentConfig) {
this.config = config;
this.factorHistory = new Map();
}
// Calculate sentiment momentum factor
sentimentMomentum(sentimentHistory, window = 5) {
if (sentimentHistory.length < window) return 0;
const recent = sentimentHistory.slice(-window);
const older = sentimentHistory.slice(-window * 2, -window);
const recentAvg = recent.reduce((a, b) => a + b.score, 0) / recent.length;
const olderAvg = older.length > 0
? older.reduce((a, b) => a + b.score, 0) / older.length
: recentAvg;
return recentAvg - olderAvg;
}
// Calculate sentiment reversal factor
sentimentReversal(sentimentHistory, threshold = 0.7) {
if (sentimentHistory.length < 2) return 0;
const current = sentimentHistory[sentimentHistory.length - 1].score;
const previous = sentimentHistory[sentimentHistory.length - 2].score;
// Large move in opposite direction = reversal
if (Math.abs(current) > threshold && Math.sign(current) !== Math.sign(previous)) {
return -current; // Contrarian
}
return 0;
}
// Calculate sentiment dispersion (disagreement among sources)
sentimentDispersion(observations) {
if (observations.length < 2) return 0;
const scores = observations.map(o => o.score);
const mean = scores.reduce((a, b) => a + b, 0) / scores.length;
const variance = scores.reduce((a, b) => a + (b - mean) ** 2, 0) / scores.length;
return Math.sqrt(variance);
}
// Calculate composite alpha factor
calculateAlpha(symbol, aggregator) {
const history = aggregator.sentimentHistory.get(symbol);
if (!history || history.length < 5) {
return { alpha: 0, confidence: 0, factors: {} };
}
const sentiment = aggregator.getAggregatedSentiment(symbol);
const momentum = this.sentimentMomentum(history);
const reversal = this.sentimentReversal(history);
const dispersion = this.sentimentDispersion(history.slice(-10));
// Composite alpha
const levelWeight = 0.4;
const momentumWeight = 0.3;
const reversalWeight = 0.2;
const dispersionPenalty = 0.1;
const alpha = (
levelWeight * sentiment.score +
momentumWeight * momentum +
reversalWeight * reversal -
dispersionPenalty * dispersion
);
const confidence = sentiment.confidence * (1 - 0.5 * dispersion);
return {
alpha: Math.max(-1, Math.min(1, alpha)),
confidence,
factors: {
level: sentiment.score,
momentum,
reversal,
dispersion
}
};
}
}
/**
* Generate synthetic news for testing
*/
function generateSyntheticNews(symbols, numItems, seed = 42) {
let rng = seed;
const random = () => { rng = (rng * 9301 + 49297) % 233280; return rng / 233280; };
const headlines = {
positive: [
'{symbol} reports strong quarterly earnings, beats estimates',
'{symbol} announces major partnership, stock surges',
'Analysts upgrade {symbol} citing growth momentum',
'{symbol} expands into new markets, revenue growth expected',
'{symbol} innovation breakthrough drives optimistic outlook',
'Record demand for {symbol} products exceeds forecasts'
],
negative: [
'{symbol} misses earnings expectations, guidance lowered',
'{symbol} faces regulatory concerns, shares decline',
'Analysts downgrade {symbol} amid market uncertainty',
'{symbol} announces layoffs as demand weakens',
'{symbol} warns of supply chain risks impacting profits',
'Investor concern grows over {symbol} debt levels'
],
neutral: [
'{symbol} maintains steady performance in Q4',
'{symbol} announces routine management changes',
'{symbol} confirms participation in industry conference',
'{symbol} files standard regulatory documents'
]
};
const sources = ['news', 'social', 'analyst', 'earnings'];
const news = [];
for (let i = 0; i < numItems; i++) {
const symbol = symbols[Math.floor(random() * symbols.length)];
const sentiment = random();
let category;
if (sentiment < 0.35) category = 'negative';
else if (sentiment < 0.65) category = 'neutral';
else category = 'positive';
const templates = headlines[category];
const headline = templates[Math.floor(random() * templates.length)]
.replace('{symbol}', symbol);
news.push({
symbol,
headline,
source: sources[Math.floor(random() * sources.length)],
timestamp: Date.now() - Math.floor(random() * 7 * 24 * 60 * 60 * 1000)
});
}
return news;
}
async function main() {
console.log('═'.repeat(70));
console.log('SENTIMENT ALPHA PIPELINE');
console.log('═'.repeat(70));
console.log();
// 1. Initialize analyzers
console.log('1. Analyzer Initialization:');
console.log('─'.repeat(70));
const lexicon = new LexiconAnalyzer();
const embedding = new EmbeddingAnalyzer();
const stream = new NewsSentimentStream();
const alphaCalc = new AlphaFactorCalculator();
console.log(' Lexicon Analyzer: Financial sentiment lexicon loaded');
console.log(' Embedding Analyzer: 64-dim embeddings configured');
console.log(' Stream Processor: Ready for real-time processing');
console.log();
// 2. Test lexicon analysis
console.log('2. Lexicon Analysis Examples:');
console.log('─'.repeat(70));
const testTexts = [
'Strong earnings beat expectations, revenue growth accelerates',
'Company warns of significant losses amid declining demand',
'Quarterly results in line with modest estimates'
];
for (const text of testTexts) {
const result = lexicon.analyze(text);
const sentiment = result.score > 0.3 ? 'Positive' : result.score < -0.3 ? 'Negative' : 'Neutral';
console.log(` "${text.substring(0, 50)}..."`);
console.log(` → Score: ${result.score.toFixed(3)}, Confidence: ${result.confidence.toFixed(2)}, ${sentiment}`);
console.log();
}
// 3. Generate and process synthetic news
console.log('3. Synthetic News Processing:');
console.log('─'.repeat(70));
const symbols = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA'];
const news = generateSyntheticNews(symbols, 50);
const processed = stream.processBatch(news);
console.log(` Processed ${processed.length} news items`);
console.log(` Symbols tracked: ${symbols.join(', ')}`);
console.log();
// 4. Aggregated sentiment
console.log('4. Aggregated Sentiment by Symbol:');
console.log('─'.repeat(70));
console.log(' Symbol │ Score │ Confidence │ Count │ Dominant Source');
console.log('─'.repeat(70));
for (const symbol of symbols) {
const agg = stream.aggregator.getAggregatedSentiment(symbol);
const dominant = agg.dominant || 'N/A';
console.log(` ${symbol.padEnd(6)}${agg.score.toFixed(3).padStart(7)}${agg.confidence.toFixed(2).padStart(10)}${agg.count.toString().padStart(5)}${dominant}`);
}
console.log();
// 5. Trading signals
console.log('5. Trading Signals:');
console.log('─'.repeat(70));
console.log(' Symbol │ Signal │ Strength │ Calibrated Prob');
console.log('─'.repeat(70));
const signals = stream.getAllSignals();
for (const [symbol, sig] of Object.entries(signals)) {
const prob = sig.calibratedProbability ? (sig.calibratedProbability * 100).toFixed(1) + '%' : 'N/A';
console.log(` ${symbol.padEnd(6)}${(sig.signal || 'HOLD').padEnd(12)}${(sig.strength || 'low').padEnd(8)}${prob}`);
}
console.log();
// 6. Alpha factors
console.log('6. Alpha Factor Analysis:');
console.log('─'.repeat(70));
console.log(' Symbol │ Alpha │ Conf │ Level │ Momentum │ Dispersion');
console.log('─'.repeat(70));
for (const symbol of symbols) {
const alpha = alphaCalc.calculateAlpha(symbol, stream.aggregator);
if (alpha.factors.level !== undefined) {
console.log(` ${symbol.padEnd(6)}${alpha.alpha.toFixed(3).padStart(6)}${alpha.confidence.toFixed(2).padStart(5)}${alpha.factors.level.toFixed(3).padStart(6)}${alpha.factors.momentum.toFixed(3).padStart(8)}${alpha.factors.dispersion.toFixed(3).padStart(10)}`);
}
}
console.log();
// 7. Recent alerts
console.log('7. Recent Sentiment Alerts:');
console.log('─'.repeat(70));
const alerts = stream.getAlerts(5);
if (alerts.length > 0) {
for (const alert of alerts) {
const direction = alert.score > 0 ? '↑' : '↓';
console.log(` ${direction} ${alert.symbol}: ${alert.headline}`);
}
} else {
console.log(' No significant sentiment alerts');
}
console.log();
// 8. Integration example
console.log('8. Kelly Criterion Integration Example:');
console.log('─'.repeat(70));
// Simulated odds for AAPL
const aaplSignal = signals['AAPL'];
if (aaplSignal && aaplSignal.calibratedProbability) {
const decimalOdds = 2.0; // Even money
const winProb = aaplSignal.calibratedProbability;
// Calculate Kelly
const b = decimalOdds - 1;
const fullKelly = (b * winProb - (1 - winProb)) / b;
const fifthKelly = fullKelly * 0.2;
console.log(` AAPL Signal: ${aaplSignal.signal}`);
console.log(` Calibrated Win Prob: ${(winProb * 100).toFixed(1)}%`);
console.log(` At 2.0 odds (even money):`);
console.log(` Full Kelly: ${(fullKelly * 100).toFixed(2)}%`);
console.log(` 1/5th Kelly: ${(fifthKelly * 100).toFixed(2)}%`);
if (fifthKelly > 0) {
console.log(` → Recommended: BET ${(fifthKelly * 100).toFixed(1)}% of bankroll`);
} else {
console.log(` → Recommended: NO BET (negative EV)`);
}
}
console.log();
console.log('═'.repeat(70));
console.log('Sentiment Alpha Pipeline demonstration completed');
console.log('═'.repeat(70));
}
export {
SentimentAggregator,
NewsSentimentStream,
AlphaFactorCalculator,
LexiconAnalyzer,
EmbeddingAnalyzer,
sentimentConfig
};
main().catch(console.error);