Add indicator signals feature with buy/sell/hold analysis
- Add signals-calculator.js module for calculating buy/sell/hold signals for all indicators - Integrate signals into Trend Analysis panel (renamed to Indicator Analysis) - Display individual indicator signals with badges, values, strength bars, and detailed reasoning - Add aggregate summary signal showing overall recommendation from all indicators - Support signals for RSI, MACD, Stochastic, Bollinger Bands, SMA/EMA, ATR, and HTS - Provide tooltips on hover showing indicator value, configuration, and reasoning - Ensure indicators calculate on all available candles, not just recent ones - Cache indicator calculations for performance while recalculating on historical data loads - Style improvements: monospace font, consistent button widths, reduced margins - Add AGENTS.md documentation file with project guidelines
This commit is contained in:
@ -1,7 +1,8 @@
|
||||
import { INTERVALS, COLORS } from '../core/index.js';
|
||||
import { calculateAllIndicatorSignals, calculateSummarySignal } from './signals-calculator.js';
|
||||
|
||||
export class TradingDashboard {
|
||||
constructor() {
|
||||
constructor() {
|
||||
this.chart = null;
|
||||
this.candleSeries = null;
|
||||
this.currentInterval = '1d';
|
||||
@ -10,6 +11,8 @@ export class TradingDashboard {
|
||||
this.isLoading = false;
|
||||
this.hasInitialLoad = false;
|
||||
this.taData = null;
|
||||
this.indicatorSignals = [];
|
||||
this.summarySignal = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
@ -286,7 +289,7 @@ export class TradingDashboard {
|
||||
|
||||
async loadInitialData() {
|
||||
await Promise.all([
|
||||
this.loadData(1000, true),
|
||||
this.loadData(2000, true),
|
||||
this.loadStats()
|
||||
]);
|
||||
this.hasInitialLoad = true;
|
||||
@ -340,14 +343,14 @@ if (data.candles && data.candles.length > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
async loadNewData() {
|
||||
async loadNewData() {
|
||||
if (!this.hasInitialLoad || this.isLoading) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.candles && data.candles.length > 0) {
|
||||
if (data.candles && data.candles.length > 0) {
|
||||
const atEdge = this.isAtRightEdge();
|
||||
|
||||
const currentSeriesData = this.candleSeries.data();
|
||||
@ -373,6 +376,8 @@ if (data.candles && data.candles.length > 0) {
|
||||
const existingData = this.allData.get(this.currentInterval) || [];
|
||||
this.allData.set(this.currentInterval, this.mergeData(existingData, chartData));
|
||||
|
||||
console.log(`[NewData Load] Added ${chartData.length} new candles, total in dataset: ${this.allData.get(this.currentInterval).length}`);
|
||||
|
||||
if (atEdge) {
|
||||
this.chart.timeScale().scrollToRealTime();
|
||||
}
|
||||
@ -380,10 +385,11 @@ if (data.candles && data.candles.length > 0) {
|
||||
const latest = chartData[chartData.length - 1];
|
||||
this.updateStats(latest);
|
||||
|
||||
// Redraw indicators when new data loads
|
||||
// Recalculate indicators and signals when new data loads
|
||||
if (window.drawIndicatorsOnChart) {
|
||||
window.drawIndicatorsOnChart();
|
||||
}
|
||||
await this.loadSignals();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading new data:', error);
|
||||
@ -397,7 +403,7 @@ if (data.candles && data.candles.length > 0) {
|
||||
return Array.from(dataMap.values()).sort((a, b) => a.time - b.time);
|
||||
}
|
||||
|
||||
onVisibleRangeChange() {
|
||||
onVisibleRangeChange() {
|
||||
if (!this.hasInitialLoad || this.isLoading) {
|
||||
return;
|
||||
}
|
||||
@ -408,6 +414,8 @@ if (data.candles && data.candles.length > 0) {
|
||||
}
|
||||
|
||||
const data = this.candleSeries.data();
|
||||
const allData = this.allData.get(this.currentInterval);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return;
|
||||
}
|
||||
@ -416,22 +424,37 @@ if (data.candles && data.candles.length > 0) {
|
||||
const bufferSize = visibleBars * 2;
|
||||
const refillThreshold = bufferSize * 0.8;
|
||||
const barsFromLeft = Math.floor(visibleRange.from);
|
||||
const visibleOldestTime = data[Math.floor(visibleRange.from)]?.time;
|
||||
const visibleNewestTime = data[Math.ceil(visibleRange.to)]?.time;
|
||||
|
||||
console.log(`[VisibleRange] Visible: ${visibleBars} bars (${data.length} in chart, ${allData?.length || 0} in dataset)`);
|
||||
console.log(`[VisibleRange] Time range: ${new Date((visibleOldestTime || 0) * 1000).toLocaleDateString()} to ${new Date((visibleNewestTime || 0) * 1000).toLocaleDateString()}`);
|
||||
|
||||
if (barsFromLeft < refillThreshold) {
|
||||
console.log(`Buffer low (${barsFromLeft} < ${refillThreshold.toFixed(0)}), silently prefetching ${bufferSize} candles...`);
|
||||
console.log(`Buffer low (${barsFromLeft} < ${refillThreshold.toFixed(0)}), prefetching ${bufferSize} candles...`);
|
||||
const oldestCandle = data[0];
|
||||
if (oldestCandle) {
|
||||
this.loadHistoricalData(oldestCandle.time, bufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate indicators when data changes
|
||||
if (data.length !== allData?.length) {
|
||||
console.log(`[VisibleRange] Chart data (${data.length}) vs dataset (${allData?.length || 0}) differ, redrawing indicators...`);
|
||||
}
|
||||
|
||||
window.drawIndicatorsOnChart?.();
|
||||
this.loadSignals().catch(e => console.error('Error loading signals:', e));
|
||||
}
|
||||
|
||||
async loadHistoricalData(beforeTime, limit = 1000) {
|
||||
async loadHistoricalData(beforeTime, limit = 1000) {
|
||||
if (this.isLoading) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
|
||||
console.log(`[Historical] Loading historical data before ${new Date(beforeTime * 1000).toLocaleDateString()}, limit=${limit}`);
|
||||
|
||||
try {
|
||||
const endTime = new Date((beforeTime - 1) * 1000);
|
||||
|
||||
@ -459,17 +482,24 @@ if (data.candles && data.candles.length > 0) {
|
||||
const mergedData = this.mergeData(existingData, chartData);
|
||||
this.allData.set(this.currentInterval, mergedData);
|
||||
|
||||
console.log(`[Historical] SUCCESS: Added ${chartData.length} candles`);
|
||||
console.log(`[Historical] Total candles in dataset: ${mergedData.length}`);
|
||||
console.log(`[Historical] Oldest: ${new Date(mergedData[0]?.time * 1000).toLocaleDateString()}`);
|
||||
console.log(`[Historical] Newest: ${new Date(mergedData[mergedData.length - 1]?.time * 1000).toLocaleDateString()}`);
|
||||
|
||||
this.candleSeries.setData(mergedData);
|
||||
|
||||
// Recalculate indicators with the expanded dataset
|
||||
// Recalculate indicators and signals with the expanded dataset
|
||||
console.log(`[Historical] Recalculating indicators...`);
|
||||
window.drawIndicatorsOnChart?.();
|
||||
await this.loadSignals();
|
||||
|
||||
console.log(`Prefetched ${chartData.length} candles, total: ${mergedData.length}`);
|
||||
console.log(`[Historical] Indicators recalculated for ${mergedData.length} candles`);
|
||||
} else {
|
||||
console.log('No more historical data available');
|
||||
console.log('[Historical] No more historical data available from database');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading historical data:', error);
|
||||
console.error('[Historical] Error loading historical data:', error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
@ -492,6 +522,7 @@ async loadTA() {
|
||||
}
|
||||
|
||||
this.taData = data;
|
||||
await this.loadSignals();
|
||||
this.renderTA();
|
||||
} catch (error) {
|
||||
console.error('Error loading TA:', error);
|
||||
@ -499,6 +530,17 @@ async loadTA() {
|
||||
}
|
||||
}
|
||||
|
||||
async loadSignals() {
|
||||
try {
|
||||
this.indicatorSignals = calculateAllIndicatorSignals();
|
||||
this.summarySignal = calculateSummarySignal(this.indicatorSignals);
|
||||
} catch (error) {
|
||||
console.error('Error loading signals:', error);
|
||||
this.indicatorSignals = [];
|
||||
this.summarySignal = null;
|
||||
}
|
||||
}
|
||||
|
||||
renderTA() {
|
||||
if (!this.taData || this.taData.error) {
|
||||
document.getElementById('taContent').innerHTML = `<div class="ta-error">${this.taData?.error || 'No data available'}</div>`;
|
||||
@ -515,14 +557,35 @@ renderTA() {
|
||||
document.getElementById('taInterval').textContent = this.currentInterval.toUpperCase();
|
||||
document.getElementById('taLastUpdate').textContent = new Date().toLocaleTimeString();
|
||||
|
||||
const summary = this.summarySignal || {};
|
||||
const summarySignalClass = summary.signal || 'hold';
|
||||
|
||||
const signalsHtml = this.indicatorSignals?.length > 0 ? this.indicatorSignals.map(indSignal => {
|
||||
const signalIcon = indSignal.signal === 'buy' ? '🟢' : indSignal.signal === 'sell' ? '🔴' : '🟡';
|
||||
const signalClass = indSignal.signal || 'hold';
|
||||
const valueStr = indSignal.value !== null && indSignal.value !== undefined ? indSignal.value.toFixed(2) : 'N/A';
|
||||
const indicatorConfig = indSignal.params ? `\nConfiguration: ${indSignal.params}` : '';
|
||||
const tooltipText = `Value: ${valueStr}${indicatorConfig}\n\n${indSignal.reasoning}`;
|
||||
|
||||
return `
|
||||
<div class="ta-ma-row" style="border-bottom: none; padding: 4px 0; cursor: help;">
|
||||
<span class="ta-ma-label" title="${tooltipText}">${indSignal.name}</span>
|
||||
<span class="ta-ma-value" style="display: flex; align-items: center; gap: 8px;">
|
||||
<span class="ta-signal ${signalClass}" style="font-size: 11px; padding: 2px 8px; min-width: 60px; text-align: center;" title="${tooltipText}">${signalIcon} ${indSignal.signal.toUpperCase()}</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('') : '';
|
||||
|
||||
const summaryBadge = '';
|
||||
|
||||
document.getElementById('taContent').innerHTML = `
|
||||
<div class="ta-section">
|
||||
<div class="ta-section-title">Trend Analysis</div>
|
||||
<div class="ta-trend ${trendClass}">
|
||||
${data.trend.direction} ${trendClass === 'bullish' ? '↑' : trendClass === 'bearish' ? '↓' : '→'}
|
||||
<div class="ta-section-title">
|
||||
Indicator Analysis
|
||||
${summaryBadge}
|
||||
</div>
|
||||
<div class="ta-strength">${data.trend.strength}</div>
|
||||
<span class="ta-signal ${signalClass}">${data.trend.signal}</span>
|
||||
${signalsHtml ? signalsHtml : `<div style="padding: 8px 0; color: var(--tv-text-secondary); font-size: 12px;">No indicators selected. Add indicators from the sidebar panel to view signals.</div>`}
|
||||
</div>
|
||||
|
||||
<div class="ta-section">
|
||||
@ -542,9 +605,35 @@ renderTA() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ta-section">
|
||||
<div class="ta-section-title">Support / Resistance</div>
|
||||
<div class="ta-level">
|
||||
<span class="ta-level-label">Resistance</span>
|
||||
<span class="ta-level-value">${data.levels.resistance.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="ta-level">
|
||||
<span class="ta-level-label">Support</span>
|
||||
<span class="ta-level-value">${data.levels.support.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ta-section">
|
||||
<div class="ta-section-title">Price Position</div>
|
||||
<div class="ta-position-bar">
|
||||
<div class="ta-position-marker" style="left: ${Math.min(Math.max(data.levels.position_in_range, 5), 95)}%"></div>
|
||||
</div>
|
||||
<div class="ta-strength" style="margin-top: 8px; font-size: 11px;">
|
||||
${data.levels.position_in_range.toFixed(0)}% in range
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderSignalsSection() {
|
||||
return '';
|
||||
}
|
||||
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/stats?symbol=BTC');
|
||||
|
||||
Reference in New Issue
Block a user