/** * Signal Recommendation Cards Component * Displays trade signals with confidence, reasoning, and action options */ class SignalCards { constructor(containerId, options = {}) { this.container = document.getElementById(containerId); this.options = { maxSignals: 10, showReasoning: true, onTakeTradeCallback: null, // Function to call when user clicks "Take Trade" colors: { bullish: '#3fb950', bearish: '#f85149', neutral: '#8b949e', highConf: '#3fb950', medConf: '#d29922', lowConf: '#f85149', ...options.colors }, ...options }; this.signals = []; this._init(); } _init() { this.container.innerHTML = `
TRADE RECOMMENDATIONS
Waiting for signals...
`; this.bodyEl = document.getElementById('signalsBody'); this.countEl = document.getElementById('signalCount'); // Wire up controls document.getElementById('signalSortBy').addEventListener('change', (e) => { this._sortSignals(e.target.value); this._render(); }); document.getElementById('signalRefresh').addEventListener('click', () => { if (this.options.onRefreshCallback) { this.options.onRefreshCallback(); } }); } /** * Set signals data * @param {Array} signals - Array of signal objects */ setSignals(signals) { this.signals = (signals || []).slice(0, this.options.maxSignals); this._render(); } /** * Add a new signal */ addSignal(signal) { if (!signal) return; // Check for duplicate const exists = this.signals.find(s => s.id === signal.id || (s.symbol === signal.symbol && s.pattern === signal.pattern && s.entry === signal.entry) ); if (!exists) { this.signals.unshift({ ...signal, id: signal.id || Date.now(), timestamp: signal.timestamp || Date.now() }); // Trim to max if (this.signals.length > this.options.maxSignals) { this.signals = this.signals.slice(0, this.options.maxSignals); } this._render(); } } /** * Remove a signal */ removeSignal(signalId) { this.signals = this.signals.filter(s => s.id !== signalId); this._render(); } /** * Clear all signals */ clear() { this.signals = []; this._render(); } _sortSignals(sortBy) { switch (sortBy) { case 'confidence': this.signals.sort((a, b) => (b.confidence || 0) - (a.confidence || 0)); break; case 'time': this.signals.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); break; case 'rr': this.signals.sort((a, b) => (b.riskReward || 0) - (a.riskReward || 0)); break; } } _render() { this.countEl.textContent = this.signals.length; if (this.signals.length === 0) { this.bodyEl.innerHTML = '
Waiting for signals...
'; return; } const html = this.signals.map((signal, idx) => this._renderCard(signal, idx === 0)).join(''); this.bodyEl.innerHTML = html; // Wire up take trade buttons this.bodyEl.querySelectorAll('.signal-action-btn').forEach(btn => { btn.addEventListener('click', (e) => { const signalId = parseInt(e.target.dataset.signalId); this._onTakeTrade(signalId); }); }); } _renderCard(signal, isTop) { const direction = signal.direction || (signal.type === 'LONG' ? 'long' : 'short'); const dirClass = direction === 'long' ? 'signal-long' : 'signal-short'; const topClass = isTop ? 'signal-top' : ''; const confRaw = signal.confidence != null ? signal.confidence : 0; const confidence = confRaw > 1 ? Math.round(confRaw) : Math.round(confRaw * 100); const confClass = confidence >= 70 ? 'high' : confidence >= 50 ? 'med' : 'low'; const rr = signal.riskReward || this._calculateRR(signal); const age = this._formatAge(signal.timestamp || signal.timestamp_ms); const grade = signal.grade || (confidence >= 85 ? 'A+' : confidence >= 70 ? 'A' : confidence >= 55 ? 'B' : 'C'); const qualityPct = Math.min(100, confidence); const qualityColor = confidence >= 70 ? 'var(--green)' : confidence >= 50 ? 'var(--orange)' : 'var(--red)'; const cardId = `sig-${signal.id}`; // Multi-timeframe confluence indicators const mtf = signal.mtf_confluence || {}; const mtfItems = Object.entries(mtf).filter(([,v]) => v).map(([k, v]) => { const label = k.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); const aligned = this._isMtfAligned(k, v, direction); return `
${aligned ? '✓' : '✗'} ${label} ${v}
`; }).join(''); const regime = signal.market_regime || {}; return `
${signal.symbol || '--'}
${direction === 'long' ? '↑' : '↓'} ${direction.toUpperCase()}
${grade}
${confidence}% score
ENTRY ${this._formatPrice(signal.entry || signal.entry_price)}
STOP ${this._formatPrice(signal.stopLoss || signal.suggested_sl)}
TARGET ${this._formatPrice(signal.takeProfit || signal.suggested_tp)}
R:R ${rr.toFixed(1)}
${signal.narrative ? `
📊 What's Happening
${signal.narrative}
` : ''} ${signal.thesis ? `
🎯 Trade Thesis
${signal.thesis}
` : ''} ${signal.edge ? `
Orderflow Edge
${signal.edge}
` : ''} ${mtfItems ? `
🔗 Timeframe Confluence
${mtfItems}
` : ''} ${signal.htf_context ? `
📈 Higher Timeframe
${signal.htf_context}
` : ''}
Pattern ${signal.pattern || signal.signal_type || '--'}
Model ${signal.model || '--'}
Session ${signal.session || '--'}
Bias ${signal.bias || '--'}
${signal.market_state ? `
Regime ${signal.market_state}
` : ''} ${signal.volume_context ? `
Volume ${signal.volume_context}
` : ''} ${signal.key_level_type ? `
Key Level ${signal.key_level_type} @ ${this._formatPrice(signal.key_level_price)}
` : ''}
${regime.detail ? `
${regime.state || ''} ${regime.detail}
` : ''} ${signal.session_detail ? `
${signal.session} ${signal.session_detail}
` : ''}
${signal.absorption_count ? `
Absorptions ${signal.absorption_count}x
` : ''}
Delta ${signal.delta_value != null ? (signal.delta_value > 0 ? '+' : '') + Math.round(signal.delta_value) : (signal.delta_confirm ? '✓' : '✗')}
Initiative ${signal.initiative_strength || '--'}%
${signal.book_imbalance_pct ? `
Book Imbalance ${signal.book_imbalance_pct}%
` : ''}
${signal.footprint_summary ? `
🏗 ${signal.footprint_summary}
` : ''} ${signal.invalidation ? `
🚫 Invalidation
${signal.invalidation}
` : ''} ${this.options.showReasoning && signal.reasons && signal.reasons.length ? `
🧠 Reasoning Chain
    ${signal.reasons.map(r => `
  1. ${r.replace(/^\d+\.\s*/, '')}
  2. `).join('')}
` : ''} ${signal.blockers && signal.blockers.length > 0 ? `
${signal.blockers.map(b => `
${b}
`).join('')}
` : ''}
`; } /** * Check if a multi-timeframe component aligns with the trade direction */ _isMtfAligned(key, value, direction) { const v = (value || '').toLowerCase(); if (direction === 'long') { return v.includes('bull') || v.includes('uptrend') || v.includes('higher') || v.includes('breakout') || v.includes('absorption') || v.includes('initiative') || v.includes('poc bounce'); } else { return v.includes('bear') || v.includes('downtrend') || v.includes('lower') || v.includes('breakout') || v.includes('exhaustion') || v.includes('divergence') || v.includes('sweep'); } } _buildReasoning(signal) { // No longer used — reasoning is rendered inline as an ordered list return null; } _calculateRR(signal) { const entry = signal.entry || signal.entry_price; const sl = signal.stopLoss || signal.suggested_sl; const tp = signal.takeProfit || signal.suggested_tp; if (!entry || !sl || !tp) return 0; const risk = Math.abs(entry - sl); const reward = Math.abs(tp - entry); return risk > 0 ? reward / risk : 0; } _formatPrice(price) { if (!price) return '--'; if (price >= 1000) return price.toFixed(1); if (price >= 1) return price.toFixed(2); return price.toFixed(4); } _formatAge(timestamp) { if (!timestamp) return '--'; const now = Date.now(); const diff = now - timestamp; if (diff < 60000) return 'Just now'; if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago'; if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago'; return Math.floor(diff / 86400000) + 'd ago'; } _onTakeTrade(signalId) { const signal = this.signals.find(s => s.id === signalId); if (!signal) return; // Mark as taken signal.taken = true; signal.takenAt = Date.now(); // Callback if (this.options.onTakeTradeCallback) { this.options.onTakeTradeCallback(signal); } // Update display this._render(); } destroy() { this.container.innerHTML = ''; } } // Export for use in main app window.SignalCards = SignalCards;