first hurst working, only for current TF

This commit is contained in:
2025-07-14 21:45:48 +02:00
parent f8064f2f44
commit 0c3c9ecd81
4 changed files with 103 additions and 225 deletions

168
app.py
View File

@ -3,18 +3,16 @@ import logging
import asyncio import asyncio
import os import os
import json import json
import csv
from flask import Flask, render_template, request from flask import Flask, render_template, request
from flask_socketio import SocketIO from flask_socketio import SocketIO
from binance import Client from binance import Client
import websockets import websockets
from threading import Lock from threading import Lock
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
# --- Configuration --- # --- Configuration ---
SYMBOL = 'ETHUSDT' SYMBOL = 'ETHUSDT'
# The CSV file is now the primary source of historical data. HISTORY_FILE = 'historical_data_1m.json' # Used as a cache to prevent re-downloading
HISTORY_CSV_FILE = 'ETHUSDT_1m_Binance.csv'
RESTART_TIMEOUT_S = 15 RESTART_TIMEOUT_S = 15
BINANCE_WS_URL = f"wss://stream.binance.com:9443/ws/{SYMBOL.lower()}@trade" BINANCE_WS_URL = f"wss://stream.binance.com:9443/ws/{SYMBOL.lower()}@trade"
@ -29,136 +27,57 @@ socketio = SocketIO(app, async_mode='threading')
# --- Global State --- # --- Global State ---
app_initialized = False app_initialized = False
app_init_lock = Lock() app_init_lock = Lock()
# This cache will hold the filtered historical data to be sent to the frontend.
historical_data_cache = []
# --- Helper Function for Optimized Reading --- # --- Historical Data Streaming ---
def get_last_timestamp_from_csv(filepath): def stream_historical_data(sid):
""" """
Efficiently reads the end of a CSV to get the timestamp from the last valid row. Fetches historical data in chunks and streams it to the client with progress updates.
This avoids reading the entire file into memory.
Returns a datetime object or None.
""" """
try: try:
with open(filepath, 'rb') as f: logging.info(f"Starting historical data stream for SID={sid}")
# Seek to a position near the end of the file to read a chunk. client = Client()
# 4096 bytes should be enough to contain several lines.
f.seek(0, os.SEEK_END)
filesize = f.tell()
if filesize == 0:
return None
f.seek(max(0, filesize - 4096), os.SEEK_SET) # Fetch the last 90 days of data in 6 chunks of 15 days each.
num_chunks = 6
chunk_size_days = 15
# Read the last part of the file end_date = datetime.utcnow()
lines = f.readlines() all_klines = []
if not lines:
return None
# Get the last non-empty line for i in range(num_chunks):
last_line_str = '' start_date = end_date - timedelta(days=chunk_size_days)
for line in reversed(lines):
decoded_line = line.decode('utf-8').strip()
if decoded_line:
last_line_str = decoded_line
break
if not last_line_str or 'Open time' in last_line_str: logging.info(f"Fetching chunk {i + 1}/{num_chunks} ({start_date} to {end_date}) for SID={sid}")
return None new_klines = client.get_historical_klines(SYMBOL, Client.KLINE_INTERVAL_1MINUTE, str(start_date), str(end_date))
last_row = last_line_str.split(',') if new_klines:
dt_obj = datetime.strptime(last_row[0], '%Y-%m-%d %H:%M:%S') all_klines.extend(new_klines)
return dt_obj.replace(tzinfo=timezone.utc)
except (IOError, IndexError, ValueError) as e: progress_payload = {
logging.error(f"Could not get last timestamp from CSV: {e}") 'progress': ((i + 1) / num_chunks) * 100
return None }
socketio.emit('history_progress', progress_payload, to=sid)
# --- Data Management --- end_date = start_date
def load_and_update_data(): socketio.sleep(0.05)
"""
Loads historical data from the CSV, updates it with the latest data from Binance,
and then filters it for the frontend.
"""
global historical_data_cache
client = Client()
# 1. Check if the primary CSV data source exists. seen = set()
if not os.path.exists(HISTORY_CSV_FILE): unique_klines = []
logging.critical(f"CRITICAL: History file '{HISTORY_CSV_FILE}' not found. Please provide the CSV file. Halting data load.") for kline in sorted(all_klines, key=lambda x: x[0]):
historical_data_cache = [] kline_tuple = tuple(kline)
return if kline_tuple not in seen:
unique_klines.append(kline)
seen.add(kline_tuple)
# 2. OPTIMIZED: Efficiently get the last timestamp to determine where to start fetching. with open(HISTORY_FILE, 'w') as f:
last_dt_in_csv = get_last_timestamp_from_csv(HISTORY_CSV_FILE) json.dump(unique_klines, f)
start_fetch_date = None logging.info(f"Finished data stream for SID={sid}. Sending final payload of {len(unique_klines)} klines.")
if last_dt_in_csv: socketio.emit('history_finished', {'klines_1m': unique_klines}, to=sid)
start_fetch_date = last_dt_in_csv + timedelta(minutes=1)
logging.info(f"Last record in CSV is from {last_dt_in_csv}. Checking for new data since {start_fetch_date}.")
else:
logging.warning("Could not determine last timestamp from CSV. Assuming file is new or empty. No new data will be fetched.")
# 3. Fetch new data from Binance.
new_klines = []
if start_fetch_date and start_fetch_date < datetime.now(timezone.utc):
while True:
logging.info(f"Fetching new klines from {start_fetch_date}...")
fetched = client.get_historical_klines(SYMBOL, Client.KLINE_INTERVAL_1MINUTE, start_fetch_date.strftime("%Y-%m-%d %H:%M:%S"))
if not fetched:
logging.info("No new klines to fetch.")
break
new_klines.extend(fetched)
last_fetched_dt = datetime.fromtimestamp(fetched[-1][0] / 1000, tz=timezone.utc)
start_fetch_date = last_fetched_dt + timedelta(minutes=1)
logging.info(f"Fetched {len(fetched)} new klines, up to {last_fetched_dt}.")
if len(fetched) < 1000:
break
time.sleep(0.1)
# 4. If new data was found, append it to the CSV file.
if new_klines:
logging.info(f"Appending {len(new_klines)} new candles to {HISTORY_CSV_FILE}.")
try:
with open(HISTORY_CSV_FILE, 'a', newline='') as f:
writer = csv.writer(f)
for kline in new_klines:
open_time_dt = datetime.fromtimestamp(kline[0] / 1000, tz=timezone.utc)
open_time_str = open_time_dt.strftime('%Y-%m-%d %H:%M:%S')
close_time_dt = datetime.fromtimestamp(kline[6] / 1000, tz=timezone.utc)
close_time_str = close_time_dt.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
writer.writerow([open_time_str] + kline[1:6] + [close_time_str] + kline[7:])
except Exception as e:
logging.error(f"Failed to append new data to {HISTORY_CSV_FILE}: {e}")
# 5. OPTIMIZED: Read the CSV and load only the necessary data (2025 onwards) for the frontend.
logging.info("Reading CSV to populate cache with data from 01.01.2025 onwards...")
frontend_klines = []
frontend_start_dt = datetime(2025, 1, 1, tzinfo=timezone.utc)
try:
with open(HISTORY_CSV_FILE, 'r', newline='') as f:
reader = csv.reader(f)
next(reader) # Skip header
for row in reader:
try:
dt_obj = datetime.strptime(row[0], '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
if dt_obj >= frontend_start_dt:
timestamp_ms = int(dt_obj.timestamp() * 1000)
frontend_klines.append([
timestamp_ms, row[1], row[2], row[3], row[4],
"0", "0", "0", "0", "0", "0"
])
except (ValueError, IndexError):
continue
historical_data_cache = frontend_klines
logging.info(f"--- Data initialization complete. {len(historical_data_cache)} candles cached for frontend. ---")
except Exception as e: except Exception as e:
logging.error(f"Failed to read CSV for frontend cache: {e}") logging.error(f"Error in stream_historical_data for SID={sid}: {e}", exc_info=True)
historical_data_cache = [] socketio.emit('history_error', {'message': str(e)}, to=sid)
# --- Real-time Data Listener --- # --- Real-time Data Listener ---
@ -183,21 +102,12 @@ def binance_listener_thread():
def handle_connect(): def handle_connect():
global app_initialized global app_initialized
logging.info(f"Client connected: IP={request.remote_addr}, SID={request.sid}") logging.info(f"Client connected: IP={request.remote_addr}, SID={request.sid}")
with app_init_lock: with app_init_lock:
if not app_initialized: if not app_initialized:
logging.info("--- First client connected, initializing application data ---") logging.info("--- Initializing Application ---")
socketio.start_background_task(load_and_update_data)
socketio.start_background_task(binance_listener_thread) socketio.start_background_task(binance_listener_thread)
app_initialized = True app_initialized = True
socketio.start_background_task(target=stream_historical_data, sid=request.sid)
# Wait until the cache is populated.
while not historical_data_cache:
logging.info(f"SID={request.sid} is waiting for historical data cache...")
socketio.sleep(1)
logging.info(f"Sending {len(historical_data_cache)} cached klines to SID={request.sid}")
socketio.emit('history_finished', {'klines_1m': historical_data_cache}, to=request.sid)
@socketio.on('analyze_chart') @socketio.on('analyze_chart')

1
historical_data_1m.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -2,13 +2,10 @@
* Creates and manages all indicator-related logic for the chart. * Creates and manages all indicator-related logic for the chart.
* @param {Object} chart - The Lightweight Charts instance. * @param {Object} chart - The Lightweight Charts instance.
* @param {Array<Object>} baseCandleData - A reference to the array holding the chart's BASE 1m candle data. * @param {Array<Object>} baseCandleData - A reference to the array holding the chart's BASE 1m candle data.
* @param {Array<Object>} displayedCandleData - A reference to the array with currently visible candles.
* @returns {Object} A manager object with public methods to control indicators. * @returns {Object} A manager object with public methods to control indicators.
*/ */
function createIndicatorManager(chart, baseCandleData) { function createIndicatorManager(chart, baseCandleData, displayedCandleData) {
// 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 = [ const indicatorSlots = [
{ id: 1, cellId: 'indicator-cell-1', series: [], definition: null, params: {} }, { id: 1, cellId: 'indicator-cell-1', series: [], definition: null, params: {} },
{ id: 2, cellId: 'indicator-cell-2', series: [], definition: null, params: {} }, { id: 2, cellId: 'indicator-cell-2', series: [], definition: null, params: {} },
@ -16,17 +13,14 @@ function createIndicatorManager(chart, baseCandleData) {
{ id: 4, cellId: 'indicator-cell-4', series: [], definition: null, params: {} }, { id: 4, cellId: 'indicator-cell-4', series: [], definition: null, params: {} },
]; ];
// Pre-defined colors for the indicator lines.
const colors = { const colors = {
bb1: { upper: '#FF9800', lower: '#FF9800' }, // Orange bb1: { upper: '#FF9800', lower: '#FF9800' },
bb2: { upper: '#2196F3', lower: '#2196F3' }, // Blue bb2: { upper: '#2196F3', lower: '#2196F3' },
bb3: { upper: '#9C27B0', lower: '#9C27B0' }, // Purple bb3: { upper: '#9C27B0', lower: '#9C27B0' },
default: ['#FF5722', '#03A9F4', '#8BC34A', '#F44336'] // Fallback colors for other indicators hurst: { topBand: '#673ab7', bottomBand: '#673ab7' },
default: ['#FF5722', '#03A9F4', '#8BC34A', '#F44336']
}; };
/**
* Populates the dropdown menus in each indicator cell.
*/
function populateDropdowns() { function populateDropdowns() {
indicatorSlots.forEach(slot => { indicatorSlots.forEach(slot => {
const cell = document.getElementById(slot.cellId); const cell = document.getElementById(slot.cellId);
@ -41,30 +35,20 @@ function createIndicatorManager(chart, baseCandleData) {
const controlsContainer = document.createElement('div'); const controlsContainer = document.createElement('div');
controlsContainer.className = 'indicator-controls'; controlsContainer.className = 'indicator-controls';
cell.innerHTML = ''; // Clear previous content cell.innerHTML = '';
cell.appendChild(select); cell.appendChild(select);
cell.appendChild(controlsContainer); cell.appendChild(controlsContainer);
select.addEventListener('change', (e) => { select.addEventListener('change', (e) => loadIndicator(slot.id, e.target.value));
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) { function loadIndicator(slotId, indicatorName) {
const slot = indicatorSlots.find(s => s.id === slotId); const slot = indicatorSlots.find(s => s.id === slotId);
if (!slot) return; if (!slot) return;
// Clean up any previous indicator series in this slot
slot.series.forEach(s => chart.removeSeries(s)); slot.series.forEach(s => chart.removeSeries(s));
slot.series = []; slot.series = [];
slot.definition = null; slot.definition = null;
slot.params = {}; slot.params = {};
@ -78,7 +62,6 @@ function createIndicatorManager(chart, baseCandleData) {
slot.definition = definition; slot.definition = definition;
// Create UI controls for the indicator's parameters
definition.params.forEach(param => { definition.params.forEach(param => {
const label = document.createElement('label'); const label = document.createElement('label');
label.textContent = param.label || param.name; label.textContent = param.label || param.name;
@ -90,7 +73,6 @@ function createIndicatorManager(chart, baseCandleData) {
if (param.min !== undefined) input.min = param.min; if (param.min !== undefined) input.min = param.min;
if (param.step !== undefined) input.step = param.step; if (param.step !== undefined) input.step = param.step;
input.className = 'input-field'; input.className = 'input-field';
input.placeholder = param.name;
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value; slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
let debounceTimer; let debounceTimer;
@ -112,37 +94,25 @@ function createIndicatorManager(chart, baseCandleData) {
updateIndicator(slot.id); 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) { function updateIndicator(slotId) {
const slot = indicatorSlots.find(s => s.id === slotId); const slot = indicatorSlots.find(s => s.id === slotId);
const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleData : displayedCandleData;
const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleData : currentAggregatedData; if (!slot || !slot.definition || candleDataForCalc.length === 0) return;
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.forEach(s => chart.removeSeries(s));
slot.series = []; slot.series = [];
console.log(`Recalculating ${slot.definition.name} for slot ${slot.id} on ${candleDataForCalc.length} candles.`);
const indicatorResult = slot.definition.calculateFull(candleDataForCalc, slot.params); const indicatorResult = slot.definition.calculateFull(candleDataForCalc, slot.params);
// Handle multi-line indicators like Bollinger Bands
if (typeof indicatorResult === 'object' && !Array.isArray(indicatorResult)) { if (typeof indicatorResult === 'object' && !Array.isArray(indicatorResult)) {
Object.keys(indicatorResult).forEach(key => { Object.keys(indicatorResult).forEach(key => {
const seriesData = indicatorResult[key]; const seriesData = indicatorResult[key];
const bandName = key.split('_')[0]; const indicatorNameLower = slot.definition.name.toLowerCase();
const bandType = key.split('_')[1];
const series = chart.addLineSeries({ const series = chart.addLineSeries({
color: colors[bandName] ? colors[bandName][bandType] : colors.default[slot.id - 1], color: (colors[indicatorNameLower] && colors[indicatorNameLower][key]) ? colors[indicatorNameLower][key] : colors.default[slot.id - 1],
lineWidth: 2, lineWidth: indicatorNameLower === 'hurst' ? 1 : 2,
title: `${slot.definition.label} - ${key}`, title: `${slot.definition.label} - ${key}`,
lastValueVisible: false, lastValueVisible: false,
priceLineVisible: false, priceLineVisible: false,
@ -150,7 +120,7 @@ function createIndicatorManager(chart, baseCandleData) {
series.setData(seriesData); series.setData(seriesData);
slot.series.push(series); slot.series.push(series);
}); });
} else { // Handle single-line indicators like SMA/EMA } else {
const series = chart.addLineSeries({ const series = chart.addLineSeries({
color: colors.default[slot.id - 1], color: colors.default[slot.id - 1],
lineWidth: 2, lineWidth: 2,
@ -161,36 +131,18 @@ function createIndicatorManager(chart, baseCandleData) {
} }
} }
/** function recalculateAllAfterHistory(newDisplayedCandleData) {
* Internal function to recalculate all active indicators. displayedCandleData = newDisplayedCandleData;
*/
function recalculateAllIndicators() {
indicatorSlots.forEach(slot => { indicatorSlots.forEach(slot => {
if (slot.definition) { if (slot.definition) updateIndicator(slot.id);
updateIndicator(slot.id);
}
}); });
} }
/** // This function is not currently used with the new model but is kept for potential future use.
* Sets the candle data for indicators and triggers a full recalculation. function updateIndicatorsOnNewCandle(newCandle) {
* @param {Array<Object>} aggregatedCandleData - The candle data for the currently selected timeframe. // Real-time updates would go here.
*/
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 { return {
populateDropdowns, populateDropdowns,
recalculateAllAfterHistory, recalculateAllAfterHistory,

View File

@ -10,6 +10,7 @@
<script src="{{ url_for('static', filename='sma.js') }}"></script> <script src="{{ url_for('static', filename='sma.js') }}"></script>
<script src="{{ url_for('static', filename='ema.js') }}"></script> <script src="{{ url_for('static', filename='ema.js') }}"></script>
<script src="{{ url_for('static', filename='bb.js') }}"></script> <script src="{{ url_for('static', filename='bb.js') }}"></script>
<script src="{{ url_for('static', filename='hurst.js') }}"></script>
<script src="{{ url_for('static', filename='indicators.js') }}"></script> <script src="{{ url_for('static', filename='indicators.js') }}"></script>
<script src="{{ url_for('static', filename='indicator-manager.js') }}"></script> <script src="{{ url_for('static', filename='indicator-manager.js') }}"></script>
@ -61,6 +62,14 @@
} }
#candle-timer { font-size: 2rem; font-weight: 500; color: var(--accent-orange); } #candle-timer { font-size: 2rem; font-weight: 500; color: var(--accent-orange); }
#timeframe-select { margin-top: 10px; } #timeframe-select { margin-top: 10px; }
.progress-bar-container {
width: 80%; height: 4px; background-color: var(--button-bg);
border-radius: 2px; margin-top: 10px; overflow: hidden;
}
.progress-bar {
width: 0%; height: 100%; background-color: var(--green);
transition: width 0.4s ease-out;
}
</style> </style>
</head> </head>
<body> <body>
@ -86,6 +95,9 @@
<option value="9">9m</option> <option value="9">9m</option>
<option value="10">10m</option> <option value="10">10m</option>
</select> </select>
<div id="progress-container" class="progress-bar-container">
<div class="progress-bar"></div>
</div>
</div> </div>
<div class="control-cell" id="indicator-cell-1"></div> <div class="control-cell" id="indicator-cell-1"></div>
<div class="control-cell" id="indicator-cell-2"></div> <div class="control-cell" id="indicator-cell-2"></div>
@ -123,27 +135,32 @@
const chartTitle = document.getElementById('chart-title'); const chartTitle = document.getElementById('chart-title');
const analyzeButton = document.getElementById('analyzeButton'); const analyzeButton = document.getElementById('analyzeButton');
const analysisResultDiv = document.getElementById('analysisResult'); const analysisResultDiv = document.getElementById('analysisResult');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.querySelector('.progress-bar');
manager = createIndicatorManager(chart, baseCandleData1m); manager = createIndicatorManager(chart, baseCandleData1m, displayedCandleData);
manager.populateDropdowns(); manager.populateDropdowns();
const socket = io(); const socket = io();
socket.on('connect', () => console.log('Socket.IO connected.')); socket.on('connect', () => console.log('Socket.IO connected.'));
socket.on('history_progress', (data) => {
if (data && data.progress) progressBar.style.width = `${data.progress}%`;
});
socket.on('history_finished', (data) => { socket.on('history_finished', (data) => {
if (!data || !data.klines_1m) return; if (!data || !data.klines_1m) return;
const mappedKlines = data.klines_1m.map(k => ({ progressBar.style.width = '100%';
baseCandleData1m = data.klines_1m.map(k => ({
time: k[0] / 1000, open: parseFloat(k[1]), high: parseFloat(k[2]), time: k[0] / 1000, open: parseFloat(k[1]), high: parseFloat(k[2]),
low: parseFloat(k[3]), close: parseFloat(k[4]) low: parseFloat(k[3]), close: parseFloat(k[4])
})); }));
baseCandleData1m.length = 0;
for (const kline of mappedKlines) {
baseCandleData1m.push(kline);
}
updateChartForTimeframe(); updateChartForTimeframe();
setTimeout(() => { progressContainer.style.display = 'none'; }, 500);
}); });
socket.on('trade', (trade) => { socket.on('trade', (trade) => {
@ -152,7 +169,10 @@
const candleTimestamp1m = tradeTime - (tradeTime % 60); const candleTimestamp1m = tradeTime - (tradeTime % 60);
if (!currentCandle1m || candleTimestamp1m > currentCandle1m.time) { if (!currentCandle1m || candleTimestamp1m > currentCandle1m.time) {
if (currentCandle1m) baseCandleData1m.push(currentCandle1m); if (currentCandle1m) {
baseCandleData1m.push(currentCandle1m);
manager.updateIndicatorsOnNewCandle(currentCandle1m);
}
currentCandle1m = { time: candleTimestamp1m, open: price, high: price, low: price, close: price }; currentCandle1m = { time: candleTimestamp1m, open: price, high: price, low: price, close: price };
} else { } else {
currentCandle1m.high = Math.max(currentCandle1m.high, price); currentCandle1m.high = Math.max(currentCandle1m.high, price);
@ -166,17 +186,11 @@
let candleForUpdate; let candleForUpdate;
if (lastDisplayedCandle && displayedCandleTimestamp === lastDisplayedCandle.time) { if (lastDisplayedCandle && displayedCandleTimestamp === lastDisplayedCandle.time) {
candleForUpdate = { ...lastDisplayedCandle }; candleForUpdate = { ...lastDisplayedCandle, high: Math.max(lastDisplayedCandle.high, price), low: Math.min(lastDisplayedCandle.low, price), close: price };
candleForUpdate.high = Math.max(candleForUpdate.high, price);
candleForUpdate.low = Math.min(candleForUpdate.low, price);
candleForUpdate.close = price;
displayedCandleData[displayedCandleData.length - 1] = candleForUpdate; displayedCandleData[displayedCandleData.length - 1] = candleForUpdate;
} else if (!lastDisplayedCandle || displayedCandleTimestamp > lastDisplayedCandle.time) { } else {
candleForUpdate = { time: displayedCandleTimestamp, open: price, high: price, low: price, close: price }; candleForUpdate = { time: displayedCandleTimestamp, open: price, high: price, low: price, close: price };
displayedCandleData.push(candleForUpdate); displayedCandleData.push(candleForUpdate);
// A new candle has started, so update the indicators.
manager.updateIndicatorsOnNewCandle(displayedCandleData);
} }
if (candleForUpdate) candlestickSeries.update(candleForUpdate); if (candleForUpdate) candlestickSeries.update(candleForUpdate);
@ -201,6 +215,7 @@
setInterval(() => { setInterval(() => {
const selectedIntervalSeconds = parseInt(timeframeSelect.value, 10) * 60; const selectedIntervalSeconds = parseInt(timeframeSelect.value, 10) * 60;
// **FIX**: Corrected syntax
const now = new Date().getTime() / 1000; const now = new Date().getTime() / 1000;
const secondsRemaining = Math.floor(selectedIntervalSeconds - (now % selectedIntervalSeconds)); const secondsRemaining = Math.floor(selectedIntervalSeconds - (now % selectedIntervalSeconds));
const minutes = Math.floor(secondsRemaining / 60); const minutes = Math.floor(secondsRemaining / 60);