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:
DiTus
2026-02-26 23:46:33 +01:00
parent cca89397cf
commit 899d9174e4
5 changed files with 870 additions and 23 deletions

View File

@ -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');