Files
OrderFlow-Analysis-Pro/orderflow_system/dashboard/static/signals.js
T
BlackboxAI 0206ef7cbb Initial commit: orderflow analysis system with 5 pattern detectors
Real-time orderflow trading system with absorption, initiative, sweep,
exhaustion, and divergence detection. Features volume profile framing,
state machine trade lifecycle, MT5 + Bybit feeds, FastAPI dashboard,
and Telegram alerts for 30+ instruments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 21:38:25 +03:00

480 lines
20 KiB
JavaScript

/**
* 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 = `
<div class="signals-container">
<div class="signals-header">
<span class="signals-title">TRADE RECOMMENDATIONS</span>
<div class="signals-filters">
<select id="signalSortBy" class="signals-select">
<option value="confidence">By Confidence</option>
<option value="time">By Time</option>
<option value="rr">By R:R</option>
</select>
<button id="signalRefresh" class="signals-btn" title="Refresh">⟳</button>
</div>
</div>
<div class="signals-body" id="signalsBody">
<div class="signals-empty">Waiting for signals...</div>
</div>
<div class="signals-footer">
<span class="signals-count">
<span id="signalCount">0</span> active signals
</span>
</div>
</div>
`;
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 = '<div class="signals-empty">Waiting for signals...</div>';
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 `<div class="mtf-item ${aligned ? 'mtf-aligned' : 'mtf-conflict'}">
<span class="mtf-check">${aligned ? '✓' : '✗'}</span>
<span class="mtf-label">${label}</span>
<span class="mtf-val">${v}</span>
</div>`;
}).join('');
const regime = signal.market_regime || {};
return `
<div class="signal-card ${dirClass} ${topClass}" data-signal-id="${signal.id}">
<!-- ── Header Row ── -->
<div class="signal-header">
<div class="signal-symbol">${signal.symbol || '--'}</div>
<div class="signal-direction">
<span class="signal-dir-arrow">${direction === 'long' ? '↑' : '↓'}</span>
<span class="signal-dir-label">${direction.toUpperCase()}</span>
</div>
<div class="signal-grade grade-${grade.replace('+', 'plus')}">${grade}</div>
<div class="signal-confidence conf-${confClass}">
<span class="signal-conf-value">${confidence}%</span>
<span class="signal-conf-label">score</span>
</div>
</div>
<div class="signal-quality-bar">
<div class="signal-quality-fill" style="width:${qualityPct}%;background:${qualityColor}"></div>
</div>
<!-- ── Price Levels ── -->
<div class="signal-levels">
<div class="signal-level signal-entry">
<span class="level-label">ENTRY</span>
<span class="level-value">${this._formatPrice(signal.entry || signal.entry_price)}</span>
</div>
<div class="signal-level signal-sl">
<span class="level-label">STOP</span>
<span class="level-value">${this._formatPrice(signal.stopLoss || signal.suggested_sl)}</span>
</div>
<div class="signal-level signal-tp">
<span class="level-label">TARGET</span>
<span class="level-value">${this._formatPrice(signal.takeProfit || signal.suggested_tp)}</span>
</div>
<div class="signal-level signal-rr">
<span class="level-label">R:R</span>
<span class="level-value">${rr.toFixed(1)}</span>
</div>
</div>
<!-- ── Narrative (what is happening) ── -->
${signal.narrative ? `
<div class="signal-section signal-narrative">
<div class="section-hdr" onclick="this.parentElement.classList.toggle('collapsed')">
<span class="section-icon">📊</span>
<span class="section-title">What's Happening</span>
<span class="section-toggle">▾</span>
</div>
<div class="section-body">${signal.narrative}</div>
</div>` : ''}
<!-- ── Trade Thesis ── -->
${signal.thesis ? `
<div class="signal-section signal-thesis">
<div class="section-hdr" onclick="this.parentElement.classList.toggle('collapsed')">
<span class="section-icon">🎯</span>
<span class="section-title">Trade Thesis</span>
<span class="section-toggle">▾</span>
</div>
<div class="section-body">${signal.thesis}</div>
</div>` : ''}
<!-- ── Edge / Why This Trade ── -->
${signal.edge ? `
<div class="signal-section signal-edge">
<div class="section-hdr" onclick="this.parentElement.classList.toggle('collapsed')">
<span class="section-icon">⚡</span>
<span class="section-title">Orderflow Edge</span>
<span class="section-toggle">▾</span>
</div>
<div class="section-body">${signal.edge}</div>
</div>` : ''}
<!-- ── Multi-Timeframe Confluence ── -->
${mtfItems ? `
<div class="signal-section signal-mtf">
<div class="section-hdr" onclick="this.parentElement.classList.toggle('collapsed')">
<span class="section-icon">🔗</span>
<span class="section-title">Timeframe Confluence</span>
<span class="section-toggle">▾</span>
</div>
<div class="section-body">
<div class="mtf-grid">${mtfItems}</div>
</div>
</div>` : ''}
<!-- ── HTF Context ── -->
${signal.htf_context ? `
<div class="signal-section signal-htf">
<div class="section-hdr" onclick="this.parentElement.classList.toggle('collapsed')">
<span class="section-icon">📈</span>
<span class="section-title">Higher Timeframe</span>
<span class="section-toggle">▾</span>
</div>
<div class="section-body">${signal.htf_context}</div>
</div>` : ''}
<!-- ── Context Grid ── -->
<div class="signal-context-grid">
<div class="ctx-chip">
<span class="ctx-key">Pattern</span>
<span class="ctx-val ctx-pattern">${signal.pattern || signal.signal_type || '--'}</span>
</div>
<div class="ctx-chip">
<span class="ctx-key">Model</span>
<span class="ctx-val">${signal.model || '--'}</span>
</div>
<div class="ctx-chip">
<span class="ctx-key">Session</span>
<span class="ctx-val">${signal.session || '--'}</span>
</div>
<div class="ctx-chip">
<span class="ctx-key">Bias</span>
<span class="ctx-val ctx-bias ${(signal.bias || '').toLowerCase()}">${signal.bias || '--'}</span>
</div>
${signal.market_state ? `<div class="ctx-chip">
<span class="ctx-key">Regime</span>
<span class="ctx-val">${signal.market_state}</span>
</div>` : ''}
${signal.volume_context ? `<div class="ctx-chip">
<span class="ctx-key">Volume</span>
<span class="ctx-val">${signal.volume_context}</span>
</div>` : ''}
${signal.key_level_type ? `<div class="ctx-chip">
<span class="ctx-key">Key Level</span>
<span class="ctx-val">${signal.key_level_type} @ ${this._formatPrice(signal.key_level_price)}</span>
</div>` : ''}
</div>
<!-- ── Regime Detail ── -->
${regime.detail ? `
<div class="signal-regime-detail">
<span class="regime-tag">${regime.state || ''}</span>
<span class="regime-text">${regime.detail}</span>
</div>` : ''}
<!-- ── Session Detail ── -->
${signal.session_detail ? `
<div class="signal-session-detail">
<span class="session-tag">${signal.session}</span>
<span class="session-text">${signal.session_detail}</span>
</div>` : ''}
<!-- ── Orderflow Metrics ── -->
<div class="signal-orderflow-metrics">
${signal.absorption_count ? `<div class="of-metric">
<span class="of-label">Absorptions</span>
<span class="of-value">${signal.absorption_count}x</span>
</div>` : ''}
<div class="of-metric">
<span class="of-label">Delta</span>
<span class="of-value ${signal.delta_confirm ? 'of-confirmed' : 'of-unconfirmed'}">
${signal.delta_value != null ? (signal.delta_value > 0 ? '+' : '') + Math.round(signal.delta_value) : (signal.delta_confirm ? '✓' : '✗')}
</span>
</div>
<div class="of-metric">
<span class="of-label">Initiative</span>
<span class="of-value">${signal.initiative_strength || '--'}%</span>
</div>
${signal.book_imbalance_pct ? `<div class="of-metric">
<span class="of-label">Book Imbalance</span>
<span class="of-value">${signal.book_imbalance_pct}%</span>
</div>` : ''}
</div>
<!-- ── Footprint Summary ── -->
${signal.footprint_summary ? `
<div class="signal-footprint-summary">
<span class="fp-icon">🏗</span>
<span class="fp-text">${signal.footprint_summary}</span>
</div>` : ''}
<!-- ── Invalidation ── -->
${signal.invalidation ? `
<div class="signal-section signal-invalidation">
<div class="section-hdr" onclick="this.parentElement.classList.toggle('collapsed')">
<span class="section-icon">🚫</span>
<span class="section-title">Invalidation</span>
<span class="section-toggle">▾</span>
</div>
<div class="section-body invalidation-text">${signal.invalidation}</div>
</div>` : ''}
<!-- ── Reasoning Chain ── -->
${this.options.showReasoning && signal.reasons && signal.reasons.length ? `
<div class="signal-section signal-reasoning-chain">
<div class="section-hdr" onclick="this.parentElement.classList.toggle('collapsed')">
<span class="section-icon">🧠</span>
<span class="section-title">Reasoning Chain</span>
<span class="section-toggle">▾</span>
</div>
<div class="section-body">
<ol class="reasoning-list">
${signal.reasons.map(r => `<li>${r.replace(/^\d+\.\s*/, '')}</li>`).join('')}
</ol>
</div>
</div>` : ''}
<!-- ── Action Footer ── -->
<div class="signal-footer">
<span class="signal-age">${age}</span>
<span class="signal-action-label ${signal.action === 'enter' ? 'action-enter' : 'action-alert'}">${signal.action === 'enter' ? '⚡ ENTRY' : '📢 ALERT'}</span>
<div class="signal-actions">
<button class="signal-action-btn signal-take-btn" data-signal-id="${signal.id}">
Take Trade
</button>
</div>
</div>
${signal.blockers && signal.blockers.length > 0 ? `
<div class="signal-blockers">
<span class="blocker-icon">⚠</span>
<div class="blocker-list">
${signal.blockers.map(b => `<div class="blocker-item">${b}</div>`).join('')}
</div>
</div>` : ''}
</div>
`;
}
/**
* 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;