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:
@ -101,6 +101,8 @@ export function setActiveIndicators(indicators) {
|
||||
renderIndicatorPanel();
|
||||
}
|
||||
|
||||
window.getActiveIndicators = getActiveIndicators;
|
||||
|
||||
// Render main panel
|
||||
export function renderIndicatorPanel() {
|
||||
const container = document.getElementById('indicatorPanel');
|
||||
@ -659,8 +661,20 @@ function saveUserPresets() {
|
||||
|
||||
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
|
||||
// Recalculate with current TF candles
|
||||
console.log(`[renderIndicatorOnPane] ${indicator.name}: START`);
|
||||
console.log(`[renderIndicatorOnPane] ${indicator.name}: Input candles = ${candles.length}`);
|
||||
console.log(`[renderIndicatorOnPane] ${indicator.name}: Calling instance.calculate()...`);
|
||||
|
||||
const results = instance.calculate(candles);
|
||||
|
||||
console.log(`[renderIndicatorOnPane] ${indicator.name}: calculate() returned ${results?.length || 0} results`);
|
||||
console.log(`[renderIndicatorOnPane] ${indicator.name}: Expected ${candles.length} results, got ${results?.length || 0}`);
|
||||
|
||||
if (results.length !== candles.length) {
|
||||
console.error(`[renderIndicatorOnPane] ${indicator.name}: MISMATCH! Expected ${candles.length} results but got ${results.length}`);
|
||||
console.error(`[renderIndicatorOnPane] ${indicator.name}: This means instance.calculate() is not returning the correct number of results!`);
|
||||
}
|
||||
|
||||
// Clear previous series for this indicator
|
||||
if (indicator.series && indicator.series.length > 0) {
|
||||
indicator.series.forEach(s => {
|
||||
@ -678,6 +692,8 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
||||
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
||||
|
||||
let plotsCreated = 0;
|
||||
let dataPointsAdded = 0;
|
||||
|
||||
meta.plots.forEach((plot, plotIdx) => {
|
||||
if (isObjectResult) {
|
||||
const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null);
|
||||
@ -687,6 +703,8 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
||||
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
||||
|
||||
const data = [];
|
||||
let firstDataIndex = -1;
|
||||
|
||||
for (let i = 0; i < candles.length; i++) {
|
||||
let value;
|
||||
if (isObjectResult) {
|
||||
@ -696,6 +714,9 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
||||
}
|
||||
|
||||
if (value !== null && value !== undefined) {
|
||||
if (firstDataIndex === -1) {
|
||||
firstDataIndex = i;
|
||||
}
|
||||
data.push({
|
||||
time: candles[i].time,
|
||||
value: value
|
||||
@ -703,7 +724,14 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
||||
}
|
||||
}
|
||||
|
||||
if (data.length === 0) return;
|
||||
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: ${data.length} data points created, first data at index ${firstDataIndex}/${candles.length}`);
|
||||
|
||||
if (data.length === 0) {
|
||||
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: No data to render`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: Creating series with ${data.length} data points [${data[0].time} to ${data[data.length - 1].time}]`);
|
||||
|
||||
let series;
|
||||
let plotLineStyle = lineStyle;
|
||||
@ -792,12 +820,26 @@ export function drawIndicatorsOnChart() {
|
||||
}
|
||||
|
||||
const currentInterval = window.dashboard.currentInterval;
|
||||
const candles = window.dashboard.allData.get(currentInterval);
|
||||
const candles = window.dashboard?.allData?.get(currentInterval);
|
||||
|
||||
if (!candles || candles.length === 0) {
|
||||
console.log('[Indicators] No candles available');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Indicators] ========== drawIndicatorsOnChart START ==========`);
|
||||
console.log(`[Indicators] Candles from allData: ${candles.length}`);
|
||||
console.log(`[Indicators] First candle time: ${candles[0]?.time} (${new Date(candles[0]?.time * 1000).toLocaleDateString()})`);
|
||||
console.log(`[Indicators] Last candle time: ${candles[candles.length - 1]?.time} (${new Date(candles[candles.length - 1]?.time * 1000).toLocaleDateString()})`);
|
||||
|
||||
const oldestTime = candles[0]?.time;
|
||||
const newestTime = candles[candles.length - 1]?.time;
|
||||
const oldestDate = oldestTime ? new Date(oldestTime * 1000).toLocaleDateString() : 'N/A';
|
||||
const newestDate = newestTime ? new Date(newestTime * 1000).toLocaleDateString() : 'N/A';
|
||||
|
||||
console.log(`[Indicators] ========== Redrawing ==========`);
|
||||
console.log(`[Indicators] Candles: ${candles.length} | Time range: ${oldestDate} (${oldestTime}) to ${newestDate} (${newestTime})`);
|
||||
|
||||
// Remove all existing series
|
||||
activeIndicators.forEach(ind => {
|
||||
ind.series?.forEach(s => {
|
||||
@ -827,9 +869,18 @@ export function drawIndicatorsOnChart() {
|
||||
const IndicatorClass = IR?.[ind.type];
|
||||
if (!IndicatorClass) return;
|
||||
|
||||
const instance = new IndicatorClass({ type: ind.type, params: ind.params, name: ind.name });
|
||||
const instance = new IndicatorClass(ind);
|
||||
const meta = instance.getMetadata();
|
||||
|
||||
// Store calculated results and metadata for signal calculation
|
||||
const results = instance.calculate(candles);
|
||||
ind.cachedResults = results;
|
||||
ind.cachedMeta = meta;
|
||||
|
||||
const validResults = results.filter(r => r !== null && r !== undefined);
|
||||
const warmupPeriod = ind.params?.period || 44;
|
||||
console.log(`[Indicators] ${ind.name}: ${validResults.length} valid results (need ${warmupPeriod} candles warmup)`);
|
||||
|
||||
if (meta.displayMode === 'pane') {
|
||||
paneIndicators.push({ indicator: ind, meta, instance });
|
||||
} else {
|
||||
@ -844,21 +895,32 @@ export function drawIndicatorsOnChart() {
|
||||
|
||||
window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight);
|
||||
|
||||
console.log(`[Indicators] ========== Rendering Indicators ==========`);
|
||||
console.log(`[Indicators] Input candles: ${candles.length} | Panel count: ${totalPanes}`);
|
||||
|
||||
overlayIndicators.forEach(({ indicator, meta, instance }) => {
|
||||
console.log(`[Indicators] Processing overlay: ${indicator.name}`);
|
||||
console.log(`[Indicators] Before renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
|
||||
renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
|
||||
console.log(`[Indicators] After renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
|
||||
});
|
||||
|
||||
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
|
||||
const paneIndex = nextPaneIndex++;
|
||||
indicatorPanes.set(indicator.id, paneIndex);
|
||||
|
||||
console.log(`[Indicators] Processing pane: ${indicator.name} (pane ${paneIndex})`);
|
||||
console.log(`[Indicators] Before renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
|
||||
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
|
||||
console.log(`[Indicators] After renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
|
||||
|
||||
const pane = window.dashboard.chart.panes()[paneIndex];
|
||||
if (pane) {
|
||||
pane.setHeight(paneHeight);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[Indicators] ========== drawIndicatorsOnChart END ==========`);
|
||||
}
|
||||
|
||||
function resetIndicator(id) {
|
||||
|
||||
Reference in New Issue
Block a user