Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
722
examples/neural-trader/production/sentiment-alpha.js
Normal file
722
examples/neural-trader/production/sentiment-alpha.js
Normal file
@@ -0,0 +1,722 @@
|
||||
/**
|
||||
* 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);
|
||||
Reference in New Issue
Block a user