Upload files to "static"
init
This commit is contained in:
50
static/candle-aggregator.js
Normal file
50
static/candle-aggregator.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Aggregates fine-grained candle data into a larger timeframe.
|
||||||
|
* For example, it can convert 1-minute candles into 5-minute candles.
|
||||||
|
*
|
||||||
|
* @param {Array<Object>} data - An array of candle objects, sorted by time.
|
||||||
|
* Each object must have { time, open, high, low, close }.
|
||||||
|
* @param {number} intervalMinutes - The desired new candle interval in minutes (e.g., 5 for 5m).
|
||||||
|
* @returns {Array<Object>} A new array of aggregated candle objects.
|
||||||
|
*/
|
||||||
|
function aggregateCandles(data, intervalMinutes) {
|
||||||
|
if (!data || data.length === 0 || !intervalMinutes || intervalMinutes < 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalSeconds = intervalMinutes * 60;
|
||||||
|
const aggregated = [];
|
||||||
|
let currentAggCandle = null;
|
||||||
|
|
||||||
|
data.forEach(candle => {
|
||||||
|
// Calculate the timestamp for the start of the interval bucket
|
||||||
|
const bucketTimestamp = candle.time - (candle.time % intervalSeconds);
|
||||||
|
|
||||||
|
if (!currentAggCandle || bucketTimestamp !== currentAggCandle.time) {
|
||||||
|
// If a previous aggregated candle exists, push it to the results
|
||||||
|
if (currentAggCandle) {
|
||||||
|
aggregated.push(currentAggCandle);
|
||||||
|
}
|
||||||
|
// Start a new aggregated candle
|
||||||
|
currentAggCandle = {
|
||||||
|
time: bucketTimestamp,
|
||||||
|
open: candle.open,
|
||||||
|
high: candle.high,
|
||||||
|
low: candle.low,
|
||||||
|
close: candle.close,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// This candle belongs to the current aggregated candle, so update it
|
||||||
|
currentAggCandle.high = Math.max(currentAggCandle.high, candle.high);
|
||||||
|
currentAggCandle.low = Math.min(currentAggCandle.low, candle.low);
|
||||||
|
currentAggCandle.close = candle.close; // The close is always the latest one
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the last aggregated candle if it exists
|
||||||
|
if (currentAggCandle) {
|
||||||
|
aggregated.push(currentAggCandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregated;
|
||||||
|
}
|
||||||
32
static/ema.js
Normal file
32
static/ema.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Indicator Definition Object for EMA.
|
||||||
|
*/
|
||||||
|
const EMA_INDICATOR = {
|
||||||
|
name: 'EMA',
|
||||||
|
label: 'Exponential Moving Average',
|
||||||
|
usesBaseData: false, // This simple indicator uses the chart's currently displayed data
|
||||||
|
params: [
|
||||||
|
{ name: 'period', type: 'number', defaultValue: 20, min: 2 },
|
||||||
|
],
|
||||||
|
calculateFull: calculateFullEMA,
|
||||||
|
};
|
||||||
|
|
||||||
|
function calculateFullEMA(data, params) {
|
||||||
|
const period = params.period;
|
||||||
|
if (!data || data.length < period) return [];
|
||||||
|
let emaData = [];
|
||||||
|
const multiplier = 2 / (period + 1);
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < period; i++) {
|
||||||
|
sum += data[i].close;
|
||||||
|
}
|
||||||
|
let prevEma = sum / period;
|
||||||
|
emaData.push({ time: data[period - 1].time, value: prevEma });
|
||||||
|
for (let i = period; i < data.length; i++) {
|
||||||
|
const close = data[i].close;
|
||||||
|
const ema = (close - prevEma) * multiplier + prevEma;
|
||||||
|
emaData.push({ time: data[i].time, value: ema });
|
||||||
|
prevEma = ema;
|
||||||
|
}
|
||||||
|
return emaData;
|
||||||
|
}
|
||||||
199
static/indicator-manager.js
Normal file
199
static/indicator-manager.js
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
};
|
||||||
|
}
|
||||||
13
static/indicators.js
Normal file
13
static/indicators.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* This file acts as a central registry for all available indicators.
|
||||||
|
* To add a new indicator to the chart UI:
|
||||||
|
* 1. Create the indicator's JS file (e.g., rsi.js) with its definition object.
|
||||||
|
* 2. Make sure that file is loaded in index.html BEFORE this one.
|
||||||
|
* 3. Add the indicator's definition object (e.g., RSI_INDICATOR) to this array.
|
||||||
|
*/
|
||||||
|
const AVAILABLE_INDICATORS = [
|
||||||
|
SMA_INDICATOR,
|
||||||
|
EMA_INDICATOR,
|
||||||
|
BB_INDICATOR, // Added the new Bollinger Bands indicator
|
||||||
|
// Add other indicators here, e.g., RSI_INDICATOR
|
||||||
|
];
|
||||||
28
static/sma.js
Normal file
28
static/sma.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Indicator Definition Object for SMA.
|
||||||
|
*/
|
||||||
|
const SMA_INDICATOR = {
|
||||||
|
name: 'SMA',
|
||||||
|
label: 'Simple Moving Average',
|
||||||
|
usesBaseData: false, // This simple indicator uses the chart's currently displayed data
|
||||||
|
params: [
|
||||||
|
{ name: 'period', type: 'number', defaultValue: 20, min: 2 },
|
||||||
|
],
|
||||||
|
calculateFull: calculateFullSMA,
|
||||||
|
};
|
||||||
|
|
||||||
|
function calculateFullSMA(data, params) {
|
||||||
|
const period = params.period;
|
||||||
|
if (!data || data.length < period) return [];
|
||||||
|
let smaData = [];
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < period; i++) {
|
||||||
|
sum += data[i].close;
|
||||||
|
}
|
||||||
|
smaData.push({ time: data[period - 1].time, value: sum / period });
|
||||||
|
for (let i = period; i < data.length; i++) {
|
||||||
|
sum = sum - data[i - period].close + data[i].close;
|
||||||
|
smaData.push({ time: data[i].time, value: sum / period });
|
||||||
|
}
|
||||||
|
return smaData;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user