Files
personal_TV/static/indicator-manager.js
2025-07-14 10:33:53 +00:00

200 lines
8.0 KiB
JavaScript

/**
* Creates and manages all indicator-related logic for the chart.
* @param {Object} chart - The Lightweight Charts instance.
* @param {Array<Object>} baseCandleData - A reference to the array holding the chart's BASE 1m candle data.
* @returns {Object} A manager object with public methods to control indicators.
*/
function createIndicatorManager(chart, baseCandleData) {
// This holds the candle data currently displayed on the chart (e.g., 5m, 10m)
let currentAggregatedData = [];
// Defines the 4 slots available in the UI for indicators.
const indicatorSlots = [
{ id: 1, cellId: 'indicator-cell-1', series: [], definition: null, params: {} },
{ id: 2, cellId: 'indicator-cell-2', series: [], definition: null, params: {} },
{ id: 3, cellId: 'indicator-cell-3', series: [], definition: null, params: {} },
{ id: 4, cellId: 'indicator-cell-4', series: [], definition: null, params: {} },
];
// Pre-defined colors for the indicator lines.
const colors = {
bb1: { upper: '#FF9800', lower: '#FF9800' }, // Orange
bb2: { upper: '#2196F3', lower: '#2196F3' }, // Blue
bb3: { upper: '#9C27B0', lower: '#9C27B0' }, // Purple
default: ['#FF5722', '#03A9F4', '#8BC34A', '#F44336'] // Fallback colors for other indicators
};
/**
* Populates the dropdown menus in each indicator cell.
*/
function populateDropdowns() {
indicatorSlots.forEach(slot => {
const cell = document.getElementById(slot.cellId);
if (!cell) return;
const select = document.createElement('select');
select.innerHTML = `<option value="">-- Select Indicator --</option>`;
AVAILABLE_INDICATORS.forEach(ind => {
select.innerHTML += `<option value="${ind.name}">${ind.label}</option>`;
});
const controlsContainer = document.createElement('div');
controlsContainer.className = 'indicator-controls';
cell.innerHTML = ''; // Clear previous content
cell.appendChild(select);
cell.appendChild(controlsContainer);
select.addEventListener('change', (e) => {
const indicatorName = e.target.value;
loadIndicator(slot.id, indicatorName);
});
});
}
/**
* Loads a new indicator into a specified slot.
* @param {number} slotId - The ID of the slot (1-4).
* @param {string} indicatorName - The name of the indicator to load (e.g., 'SMA').
*/
function loadIndicator(slotId, indicatorName) {
const slot = indicatorSlots.find(s => s.id === slotId);
if (!slot) return;
// Clean up any previous indicator series in this slot
slot.series.forEach(s => chart.removeSeries(s));
slot.series = [];
slot.definition = null;
slot.params = {};
const controlsContainer = document.querySelector(`#${slot.cellId} .indicator-controls`);
controlsContainer.innerHTML = '';
if (!indicatorName) return;
const definition = AVAILABLE_INDICATORS.find(ind => ind.name === indicatorName);
if (!definition) return;
slot.definition = definition;
// Create UI controls for the indicator's parameters
definition.params.forEach(param => {
const label = document.createElement('label');
label.textContent = param.label || param.name;
label.style.fontSize = '12px';
const input = document.createElement('input');
input.type = param.type;
input.value = param.defaultValue;
if (param.min !== undefined) input.min = param.min;
if (param.step !== undefined) input.step = param.step;
input.className = 'input-field';
input.placeholder = param.name;
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
let debounceTimer;
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
updateIndicator(slot.id);
}, 500);
});
const controlGroup = document.createElement('div');
controlGroup.style.display = 'flex';
controlGroup.style.flexDirection = 'column';
controlGroup.appendChild(label);
controlGroup.appendChild(input);
controlsContainer.appendChild(controlGroup);
});
updateIndicator(slot.id);
}
/**
* Recalculates and redraws the lines for a specific indicator.
* @param {number} slotId - The ID of the slot to update.
*/
function updateIndicator(slotId) {
const slot = indicatorSlots.find(s => s.id === slotId);
const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleData : currentAggregatedData;
if (!slot || !slot.definition || candleDataForCalc.length === 0) {
return;
}
// Clean up previous series before creating new ones
slot.series.forEach(s => chart.removeSeries(s));
slot.series = [];
console.log(`Recalculating ${slot.definition.name} for slot ${slot.id} on ${candleDataForCalc.length} candles.`);
const indicatorResult = slot.definition.calculateFull(candleDataForCalc, slot.params);
// Handle multi-line indicators like Bollinger Bands
if (typeof indicatorResult === 'object' && !Array.isArray(indicatorResult)) {
Object.keys(indicatorResult).forEach(key => {
const seriesData = indicatorResult[key];
const bandName = key.split('_')[0];
const bandType = key.split('_')[1];
const series = chart.addLineSeries({
color: colors[bandName] ? colors[bandName][bandType] : colors.default[slot.id - 1],
lineWidth: 2,
title: `${slot.definition.label} - ${key}`,
lastValueVisible: false,
priceLineVisible: false,
});
series.setData(seriesData);
slot.series.push(series);
});
} else { // Handle single-line indicators like SMA/EMA
const series = chart.addLineSeries({
color: colors.default[slot.id - 1],
lineWidth: 2,
title: slot.definition.label,
});
series.setData(indicatorResult);
slot.series.push(series);
}
}
/**
* Internal function to recalculate all active indicators.
*/
function recalculateAllIndicators() {
indicatorSlots.forEach(slot => {
if (slot.definition) {
updateIndicator(slot.id);
}
});
}
/**
* Sets the candle data for indicators and triggers a full recalculation.
* @param {Array<Object>} aggregatedCandleData - The candle data for the currently selected timeframe.
*/
function recalculateAllAfterHistory(aggregatedCandleData) {
currentAggregatedData = aggregatedCandleData;
recalculateAllIndicators();
}
/**
* Updates all indicators in response to a new candle closing.
* @param {Array<Object>} aggregatedCandleData - The latest candle data for the currently selected timeframe.
*/
function updateIndicatorsOnNewCandle(aggregatedCandleData) {
currentAggregatedData = aggregatedCandleData;
recalculateAllIndicators();
}
// Public API for the manager
return {
populateDropdowns,
recalculateAllAfterHistory,
updateIndicatorsOnNewCandle
};
}