Compare commits
6 Commits
96a96e2166
...
3843bfdff8
| Author | SHA1 | Date | |
|---|---|---|---|
| 3843bfdff8 | |||
| e88f836f9d | |||
| 80e3875abe | |||
| 0c3c9ecd81 | |||
| f8064f2f44 | |||
| 666d5fb007 |
207
app.py
207
app.py
@ -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'
|
||||||
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,141 +27,48 @@ 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.
|
current_bar = {} # To track the currently forming 1-minute candle
|
||||||
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.
|
|
||||||
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.
|
num_chunks = 6
|
||||||
f.seek(0, os.SEEK_END)
|
chunk_size_days = 15
|
||||||
filesize = f.tell()
|
end_date = datetime.utcnow()
|
||||||
if filesize == 0:
|
all_klines = []
|
||||||
return None
|
|
||||||
|
|
||||||
f.seek(max(0, filesize - 4096), os.SEEK_SET)
|
|
||||||
|
|
||||||
# Read the last part of the file
|
|
||||||
lines = f.readlines()
|
|
||||||
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):
|
logging.info(f"Fetching chunk {i + 1}/{num_chunks} for SID={sid}")
|
||||||
decoded_line = line.decode('utf-8').strip()
|
new_klines = client.get_historical_klines(SYMBOL, Client.KLINE_INTERVAL_1MINUTE, str(start_date), str(end_date))
|
||||||
if decoded_line:
|
if new_klines:
|
||||||
last_line_str = decoded_line
|
all_klines.extend(new_klines)
|
||||||
break
|
socketio.emit('history_progress', {'progress': ((i + 1) / num_chunks) * 100}, to=sid)
|
||||||
|
end_date = start_date
|
||||||
if not last_line_str or 'Open time' in last_line_str:
|
socketio.sleep(0.05)
|
||||||
return None
|
|
||||||
|
|
||||||
last_row = last_line_str.split(',')
|
seen = set()
|
||||||
dt_obj = datetime.strptime(last_row[0], '%Y-%m-%d %H:%M:%S')
|
unique_klines = [kline for kline in sorted(all_klines, key=lambda x: x[0]) if tuple(kline) not in seen and not seen.add(tuple(kline))]
|
||||||
return dt_obj.replace(tzinfo=timezone.utc)
|
|
||||||
|
|
||||||
except (IOError, IndexError, ValueError) as e:
|
|
||||||
logging.error(f"Could not get last timestamp from CSV: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# --- Data Management ---
|
|
||||||
def load_and_update_data():
|
|
||||||
"""
|
|
||||||
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.
|
|
||||||
if not os.path.exists(HISTORY_CSV_FILE):
|
|
||||||
logging.critical(f"CRITICAL: History file '{HISTORY_CSV_FILE}' not found. Please provide the CSV file. Halting data load.")
|
|
||||||
historical_data_cache = []
|
|
||||||
return
|
|
||||||
|
|
||||||
# 2. OPTIMIZED: Efficiently get the last timestamp to determine where to start fetching.
|
|
||||||
last_dt_in_csv = get_last_timestamp_from_csv(HISTORY_CSV_FILE)
|
|
||||||
|
|
||||||
start_fetch_date = None
|
|
||||||
if last_dt_in_csv:
|
|
||||||
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
|
with open(HISTORY_FILE, 'w') as f:
|
||||||
logging.info(f"--- Data initialization complete. {len(historical_data_cache)} candles cached for frontend. ---")
|
json.dump(unique_klines, f)
|
||||||
|
|
||||||
|
logging.info(f"Finished data stream for SID={sid}. Sending final payload of {len(unique_klines)} klines.")
|
||||||
|
socketio.emit('history_finished', {'klines_1m': unique_klines}, to=sid)
|
||||||
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 ---
|
||||||
def binance_listener_thread():
|
def binance_listener_thread():
|
||||||
|
"""
|
||||||
|
Connects to Binance, manages the 1-minute candle, and emits updates.
|
||||||
|
"""
|
||||||
|
global current_bar
|
||||||
async def listener():
|
async def listener():
|
||||||
|
global current_bar
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
logging.info(f"Connecting to Binance WebSocket at {BINANCE_WS_URL}...")
|
logging.info(f"Connecting to Binance WebSocket at {BINANCE_WS_URL}...")
|
||||||
@ -171,7 +76,27 @@ def binance_listener_thread():
|
|||||||
logging.info("Binance WebSocket connected successfully.")
|
logging.info("Binance WebSocket connected successfully.")
|
||||||
while True:
|
while True:
|
||||||
message = await websocket.recv()
|
message = await websocket.recv()
|
||||||
socketio.emit('trade', json.loads(message))
|
trade = json.loads(message)
|
||||||
|
|
||||||
|
price = float(trade['p'])
|
||||||
|
trade_time_s = trade['T'] // 1000
|
||||||
|
candle_timestamp = trade_time_s - (trade_time_s % 60)
|
||||||
|
|
||||||
|
if not current_bar or candle_timestamp > current_bar.get("time", 0):
|
||||||
|
if current_bar:
|
||||||
|
# The previous candle is now closed, emit it
|
||||||
|
logging.info(f"Candle closed at {current_bar['close']}. Emitting 'candle_closed' event.")
|
||||||
|
socketio.emit('candle_closed', current_bar)
|
||||||
|
|
||||||
|
current_bar = {"time": candle_timestamp, "open": price, "high": price, "low": price, "close": price}
|
||||||
|
else:
|
||||||
|
current_bar['high'] = max(current_bar.get('high', price), price)
|
||||||
|
current_bar['low'] = min(current_bar.get('low', price), price)
|
||||||
|
current_bar['close'] = price
|
||||||
|
|
||||||
|
# Emit the live, updating candle for visual feedback
|
||||||
|
socketio.emit('candle_update', current_bar)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Binance listener error: {e}. Reconnecting...")
|
logging.error(f"Binance listener error: {e}. Reconnecting...")
|
||||||
await asyncio.sleep(RESTART_TIMEOUT_S)
|
await asyncio.sleep(RESTART_TIMEOUT_S)
|
||||||
@ -183,32 +108,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')
|
|
||||||
def handle_analyze_chart(data):
|
|
||||||
sid = request.sid
|
|
||||||
logging.info(f"Received 'analyze_chart' request from frontend (SID={sid})")
|
|
||||||
recent_data = data[-100:]
|
|
||||||
prompt_data = "\n".join([f"Time: {c['time']}, Open: {c['open']}, High: {c['high']}, Low: {c['low']}, Close: {c['close']}" for c in recent_data])
|
|
||||||
prompt = (f"You are a financial analyst. Based on the following recent candlestick data for {SYMBOL}, provide a brief technical analysis (3-4 sentences). Mention the current trend and any potential short-term support or resistance levels.\n\nData:\n{prompt_data}")
|
|
||||||
socketio.emit('analysis_result', {'analysis': "AI analysis is currently unavailable."}, to=sid)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Flask Routes ---
|
# --- Flask Routes ---
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
|
|||||||
1
historical_data_1m.json
Normal file
1
historical_data_1m.json
Normal file
File diff suppressed because one or more lines are too long
223
static/hurst.js
Normal file
223
static/hurst.js
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* Indicator Definition Object for Hurst Bands (Multi-Timeframe).
|
||||||
|
* This object is used by the indicator manager to create and control the indicator.
|
||||||
|
* It defines the parameters and the calculation functions.
|
||||||
|
*/
|
||||||
|
const HURST_INDICATOR = {
|
||||||
|
name: 'Hurst',
|
||||||
|
label: 'Hurst Bands (Multi-TF)',
|
||||||
|
params: [
|
||||||
|
{ name: 'cycle', type: 'number', defaultValue: 30, min: 2 },
|
||||||
|
{ name: 'timeframe_mult', type: 'number', defaultValue: 5, min: 2, step: 1 },
|
||||||
|
],
|
||||||
|
// The output is { topBand, bottomBand, topBand_h, bottomBand_h }
|
||||||
|
calculateFull: calculateFullHurst,
|
||||||
|
createRealtime: createRealtimeHurstCalculator,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Helper Functions (private to this file) ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregates candle data into a higher timeframe.
|
||||||
|
* @param {Array<Object>} data - The original candle data.
|
||||||
|
* @param {number} multiplier - The timeframe multiplier (e.g., 5 for 5-minute candles from 1-minute data).
|
||||||
|
* @returns {Array<Object>} A new array of aggregated candle objects.
|
||||||
|
*/
|
||||||
|
function _aggregateCandles(data, multiplier) {
|
||||||
|
if (multiplier <= 1) return data;
|
||||||
|
|
||||||
|
const aggregatedData = [];
|
||||||
|
for (let i = 0; i < data.length; i += multiplier) {
|
||||||
|
const chunk = data.slice(i, i + multiplier);
|
||||||
|
if (chunk.length > 0) {
|
||||||
|
const newCandle = {
|
||||||
|
open: chunk[0].open,
|
||||||
|
high: Math.max(...chunk.map(c => c.high)),
|
||||||
|
low: Math.min(...chunk.map(c => c.low)),
|
||||||
|
close: chunk[chunk.length - 1].close,
|
||||||
|
// The timestamp of the new candle corresponds to the end of the period.
|
||||||
|
time: chunk[chunk.length - 1].time,
|
||||||
|
};
|
||||||
|
aggregatedData.push(newCandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aggregatedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates RMA (Relative Moving Average), a type of EMA.
|
||||||
|
* @param {number[]} series - An array of numbers.
|
||||||
|
* @param {number} period - The smoothing period.
|
||||||
|
* @returns {number[]} The calculated RMA series.
|
||||||
|
*/
|
||||||
|
function _calculateRMA(series, period) {
|
||||||
|
if (series.length < period) return [];
|
||||||
|
const alpha = 1 / period;
|
||||||
|
let rma = [];
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < period; i++) {
|
||||||
|
sum += series[i];
|
||||||
|
}
|
||||||
|
rma.push(sum / period);
|
||||||
|
for (let i = period; i < series.length; i++) {
|
||||||
|
const val = alpha * series[i] + (1 - alpha) * rma[rma.length - 1];
|
||||||
|
rma.push(val);
|
||||||
|
}
|
||||||
|
return rma;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates ATR (Average True Range).
|
||||||
|
* @param {Array<Object>} data - The full candle data.
|
||||||
|
* @param {number} period - The ATR period.
|
||||||
|
* @returns {number[]} The calculated ATR series.
|
||||||
|
*/
|
||||||
|
function _calculateATR(data, period) {
|
||||||
|
if (data.length < period) return [];
|
||||||
|
let tr_series = [data[0].high - data[0].low];
|
||||||
|
for (let i = 1; i < data.length; i++) {
|
||||||
|
const h = data[i].high;
|
||||||
|
const l = data[i].low;
|
||||||
|
const prev_c = data[i - 1].close;
|
||||||
|
const tr = Math.max(h - l, Math.abs(h - prev_c), Math.abs(l - prev_c));
|
||||||
|
tr_series.push(tr);
|
||||||
|
}
|
||||||
|
return _calculateRMA(tr_series, period);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic function to calculate a single set of Hurst Bands.
|
||||||
|
* This is the core calculation logic.
|
||||||
|
* @param {Array<Object>} data - An array of candle objects for a specific timeframe.
|
||||||
|
* @param {number} cycle - The cycle length for this calculation.
|
||||||
|
* @param {number} atr_mult - The ATR multiplier for this calculation.
|
||||||
|
* @returns {Object} An object containing two arrays: { topBand: [...], bottomBand: [...] }.
|
||||||
|
*/
|
||||||
|
function _calculateSingleBandSet(data, cycle, atr_mult) {
|
||||||
|
const mcl = Math.floor(cycle / 2);
|
||||||
|
const mcl_2 = Math.floor(mcl / 2);
|
||||||
|
|
||||||
|
if (data.length < cycle + mcl_2) {
|
||||||
|
return { topBand: [], bottomBand: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePrices = data.map(d => d.close);
|
||||||
|
const ma_mcl_full = _calculateRMA(closePrices, mcl);
|
||||||
|
const atr_full = _calculateATR(data, mcl);
|
||||||
|
|
||||||
|
const topBand = [];
|
||||||
|
const bottomBand = [];
|
||||||
|
const startIndex = mcl - 1 + mcl_2;
|
||||||
|
|
||||||
|
for (let i = startIndex; i < data.length; i++) {
|
||||||
|
const rma_atr_base_index = i - (mcl - 1);
|
||||||
|
const center_ma_index = rma_atr_base_index - mcl_2;
|
||||||
|
|
||||||
|
if (center_ma_index >= 0 && rma_atr_base_index >= 0) {
|
||||||
|
const center = ma_mcl_full[center_ma_index];
|
||||||
|
const offset = atr_full[rma_atr_base_index] * atr_mult;
|
||||||
|
|
||||||
|
if (center !== undefined && offset !== undefined) {
|
||||||
|
topBand.push({ time: data[i].time, value: center + offset });
|
||||||
|
bottomBand.push({ time: data[i].time, value: center - offset });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { topBand, bottomBand };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main Calculation Functions ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates both primary and higher-timeframe Hurst Bands for an entire dataset.
|
||||||
|
* @param {Array<Object>} data - An array of candle objects.
|
||||||
|
* @param {Object} params - An object with { cycle, timeframe_mult }.
|
||||||
|
* @returns {Object} An object containing four arrays: { topBand, bottomBand, topBand_h, bottomBand_h }.
|
||||||
|
*/
|
||||||
|
function calculateFullHurst(data, params) {
|
||||||
|
const { cycle, timeframe_mult } = params;
|
||||||
|
|
||||||
|
// 1. Calculate Primary Bands (e.g., 1-minute)
|
||||||
|
const primaryBands = _calculateSingleBandSet(data, cycle, 1.8);
|
||||||
|
|
||||||
|
// 2. Aggregate candles to higher timeframe (e.g., 5-minute)
|
||||||
|
const higherTfData = _aggregateCandles(data, timeframe_mult);
|
||||||
|
|
||||||
|
// 3. Calculate Higher Timeframe Bands
|
||||||
|
const higherTFBandsRaw = _calculateSingleBandSet(higherTfData, cycle, 1.9);
|
||||||
|
|
||||||
|
// 4. Align higher timeframe results back to the primary timeframe for plotting
|
||||||
|
const higherTfResults = new Map(higherTFBandsRaw.topBand.map((p, i) => [
|
||||||
|
p.time,
|
||||||
|
{ top: p.value, bottom: higherTFBandsRaw.bottomBand[i].value }
|
||||||
|
]));
|
||||||
|
|
||||||
|
const topBand_h = [];
|
||||||
|
const bottomBand_h = [];
|
||||||
|
let lastKnownTop = null;
|
||||||
|
let lastKnownBottom = null;
|
||||||
|
|
||||||
|
for (const candle of data) {
|
||||||
|
if (higherTfResults.has(candle.time)) {
|
||||||
|
const bands = higherTfResults.get(candle.time);
|
||||||
|
lastKnownTop = bands.top;
|
||||||
|
lastKnownBottom = bands.bottom;
|
||||||
|
}
|
||||||
|
// Carry forward the last known value until a new one is calculated
|
||||||
|
if (lastKnownTop !== null) {
|
||||||
|
topBand_h.push({ time: candle.time, value: lastKnownTop });
|
||||||
|
bottomBand_h.push({ time: candle.time, value: lastKnownBottom });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
topBand: primaryBands.topBand,
|
||||||
|
bottomBand: primaryBands.bottomBand,
|
||||||
|
topBand_h,
|
||||||
|
bottomBand_h,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a stateful Hurst calculator for real-time updates.
|
||||||
|
* @param {Object} params - An object with { cycle, timeframe_mult }.
|
||||||
|
* @returns {Object} A calculator object with `update` and `prime` methods.
|
||||||
|
*/
|
||||||
|
function createRealtimeHurstCalculator(params) {
|
||||||
|
const { cycle, timeframe_mult } = params;
|
||||||
|
// Buffer needs to be large enough to contain enough aggregated candles for a valid calculation.
|
||||||
|
const minHigherTfCandles = cycle + Math.floor(Math.floor(cycle / 2) / 2);
|
||||||
|
const bufferSize = minHigherTfCandles * timeframe_mult * 2; // Use a safe buffer size
|
||||||
|
let buffer = [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
update: function(candle) {
|
||||||
|
buffer.push(candle);
|
||||||
|
if (buffer.length > bufferSize) {
|
||||||
|
buffer.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's enough data for at least one calculation on the higher timeframe.
|
||||||
|
const requiredLength = minHigherTfCandles * timeframe_mult;
|
||||||
|
if (buffer.length < requiredLength) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = calculateFullHurst(buffer, params);
|
||||||
|
|
||||||
|
if (result.topBand.length > 0 && result.topBand_h.length > 0) {
|
||||||
|
return {
|
||||||
|
topBand: result.topBand[result.topBand.length - 1],
|
||||||
|
bottomBand: result.bottomBand[result.bottomBand.length - 1],
|
||||||
|
topBand_h: result.topBand_h[result.topBand_h.length - 1],
|
||||||
|
bottomBand_h: result.bottomBand_h[result.bottomBand_h.length - 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
prime: function(historicalCandles) {
|
||||||
|
buffer = historicalCandles.slice(-bufferSize);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,32 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* 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>} baseCandleDataRef - A reference to the array holding the chart's BASE 1m candle data.
|
||||||
|
* @param {Array<Object>} displayedCandleDataRef - 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, baseCandleDataRef, displayedCandleDataRef) {
|
||||||
// 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: {}, calculator: null },
|
||||||
{ id: 2, cellId: 'indicator-cell-2', series: [], definition: null, params: {} },
|
{ id: 2, cellId: 'indicator-cell-2', series: [], definition: null, params: {}, calculator: null },
|
||||||
{ id: 3, cellId: 'indicator-cell-3', series: [], definition: null, params: {} },
|
{ id: 3, cellId: 'indicator-cell-3', series: [], definition: null, params: {}, calculator: null },
|
||||||
{ id: 4, cellId: 'indicator-cell-4', series: [], definition: null, params: {} },
|
{ id: 4, cellId: 'indicator-cell-4', series: [], definition: null, params: {}, calculator: null },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Pre-defined colors for the indicator lines.
|
// **FIX**: Updated colors object to match your styling request.
|
||||||
const colors = {
|
const colors = {
|
||||||
bb1: { upper: '#FF9800', lower: '#FF9800' }, // Orange
|
bb: { bb1_upper: '#FF9800', bb1_lower: '#FF9800', bb2_upper: '#2196F3', bb2_lower: '#2196F3', bb3_upper: '#9C27B0', bb3_lower: '#9C27B0' },
|
||||||
bb2: { upper: '#2196F3', lower: '#2196F3' }, // Blue
|
hurst: { topBand: '#787b86', bottomBand: '#787b86', topBand_h: '#673ab7', bottomBand_h: '#673ab7' },
|
||||||
bb3: { upper: '#9C27B0', lower: '#9C27B0' }, // Purple
|
default: ['#00BCD4', '#FFEB3B', '#4CAF50', '#E91E63'] // Cyan, Yellow, Green, Pink
|
||||||
default: ['#FF5722', '#03A9F4', '#8BC34A', '#F44336'] // Fallback colors for other indicators
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,32 +34,23 @@ 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 = {};
|
||||||
|
slot.calculator = null;
|
||||||
|
|
||||||
const controlsContainer = document.querySelector(`#${slot.cellId} .indicator-controls`);
|
const controlsContainer = document.querySelector(`#${slot.cellId} .indicator-controls`);
|
||||||
controlsContainer.innerHTML = '';
|
controlsContainer.innerHTML = '';
|
||||||
@ -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;
|
||||||
@ -98,7 +80,7 @@ function createIndicatorManager(chart, baseCandleData) {
|
|||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
||||||
updateIndicator(slot.id);
|
updateIndicator(slot.id, true);
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
const controlGroup = document.createElement('div');
|
const controlGroup = document.createElement('div');
|
||||||
@ -109,91 +91,92 @@ function createIndicatorManager(chart, baseCandleData) {
|
|||||||
controlsContainer.appendChild(controlGroup);
|
controlsContainer.appendChild(controlGroup);
|
||||||
});
|
});
|
||||||
|
|
||||||
updateIndicator(slot.id);
|
updateIndicator(slot.id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function updateIndicator(slotId, isFullRecalculation = false) {
|
||||||
* 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 slot = indicatorSlots.find(s => s.id === slotId);
|
||||||
|
const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleDataRef : displayedCandleDataRef;
|
||||||
const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleData : currentAggregatedData;
|
|
||||||
|
|
||||||
if (!slot || !slot.definition || candleDataForCalc.length === 0) {
|
if (!slot || !slot.definition || candleDataForCalc.length === 0) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up previous series before creating new ones
|
if (isFullRecalculation) {
|
||||||
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);
|
if (typeof indicatorResult === 'object' && !Array.isArray(indicatorResult)) {
|
||||||
|
Object.keys(indicatorResult).forEach(key => {
|
||||||
// Handle multi-line indicators like Bollinger Bands
|
const seriesData = indicatorResult[key];
|
||||||
if (typeof indicatorResult === 'object' && !Array.isArray(indicatorResult)) {
|
const indicatorNameLower = slot.definition.name.toLowerCase();
|
||||||
Object.keys(indicatorResult).forEach(key => {
|
const series = chart.addLineSeries({
|
||||||
const seriesData = indicatorResult[key];
|
color: (colors[indicatorNameLower] && colors[indicatorNameLower][key]) ? colors[indicatorNameLower][key] : colors.default[slot.id - 1],
|
||||||
const bandName = key.split('_')[0];
|
lineWidth: 1, // **FIX**: Set line width to 1px
|
||||||
const bandType = key.split('_')[1];
|
title: '', // **FIX**: Remove title label
|
||||||
|
lastValueVisible: false, // **FIX**: Remove price label on the right
|
||||||
const series = chart.addLineSeries({
|
priceLineVisible: false, // **FIX**: Remove dotted horizontal line
|
||||||
color: colors[bandName] ? colors[bandName][bandType] : colors.default[slot.id - 1],
|
});
|
||||||
lineWidth: 2,
|
series.setData(seriesData);
|
||||||
title: `${slot.definition.label} - ${key}`,
|
slot.series.push(series);
|
||||||
lastValueVisible: false,
|
|
||||||
priceLineVisible: false,
|
|
||||||
});
|
});
|
||||||
series.setData(seriesData);
|
} else {
|
||||||
|
const series = chart.addLineSeries({
|
||||||
|
color: colors.default[slot.id - 1],
|
||||||
|
lineWidth: 1, // **FIX**: Set line width to 1px
|
||||||
|
title: '', // **FIX**: Remove title label
|
||||||
|
lastValueVisible: false, // **FIX**: Remove price label on the right
|
||||||
|
priceLineVisible: false, // **FIX**: Remove dotted horizontal line
|
||||||
|
});
|
||||||
|
series.setData(indicatorResult);
|
||||||
slot.series.push(series);
|
slot.series.push(series);
|
||||||
});
|
}
|
||||||
} else { // Handle single-line indicators like SMA/EMA
|
|
||||||
const series = chart.addLineSeries({
|
if (slot.definition.createRealtime) {
|
||||||
color: colors.default[slot.id - 1],
|
slot.calculator = slot.definition.createRealtime(slot.params);
|
||||||
lineWidth: 2,
|
slot.calculator.prime(candleDataForCalc);
|
||||||
title: slot.definition.label,
|
}
|
||||||
});
|
} else if (slot.calculator) {
|
||||||
series.setData(indicatorResult);
|
// **FIX**: This is the lightweight real-time update logic
|
||||||
slot.series.push(series);
|
const lastCandle = candleDataForCalc[candleDataForCalc.length - 1];
|
||||||
|
if (!lastCandle) return;
|
||||||
|
|
||||||
|
const newPoint = slot.calculator.update(lastCandle);
|
||||||
|
|
||||||
|
if (newPoint && typeof newPoint === 'object') {
|
||||||
|
if (slot.series.length > 1) { // Multi-line indicator
|
||||||
|
Object.keys(newPoint).forEach((key, index) => {
|
||||||
|
if (slot.series[index] && newPoint[key]) {
|
||||||
|
slot.series[index].update(newPoint[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (slot.series.length === 1) { // Single-line indicator
|
||||||
|
slot.series[0].update(newPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function recalculateAllAfterHistory(baseData, displayedData) {
|
||||||
* Internal function to recalculate all active indicators.
|
baseCandleDataRef = baseData;
|
||||||
*/
|
displayedCandleDataRef = displayedData;
|
||||||
function recalculateAllIndicators() {
|
|
||||||
indicatorSlots.forEach(slot => {
|
indicatorSlots.forEach(slot => {
|
||||||
|
if (slot.definition) updateIndicator(slot.id, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// **FIX**: New lightweight function for real-time updates
|
||||||
|
function updateAllOnNewCandle() {
|
||||||
|
indicatorSlots.forEach(slot => {
|
||||||
if (slot.definition) {
|
if (slot.definition) {
|
||||||
updateIndicator(slot.id);
|
updateIndicator(slot.id, false); // Perform a lightweight update
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
return {
|
||||||
populateDropdowns,
|
populateDropdowns,
|
||||||
recalculateAllAfterHistory,
|
recalculateAllAfterHistory,
|
||||||
updateIndicatorsOnNewCandle
|
updateAllOnNewCandle, // Expose the new function
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,5 +9,6 @@ const AVAILABLE_INDICATORS = [
|
|||||||
SMA_INDICATOR,
|
SMA_INDICATOR,
|
||||||
EMA_INDICATOR,
|
EMA_INDICATOR,
|
||||||
BB_INDICATOR, // Added the new Bollinger Bands indicator
|
BB_INDICATOR, // Added the new Bollinger Bands indicator
|
||||||
// Add other indicators here, e.g., RSI_INDICATOR
|
HURST_INDICATOR // Added the new Hurst Bands indicator
|
||||||
|
// Add other indicators here as needed
|
||||||
];
|
];
|
||||||
@ -9,7 +9,8 @@
|
|||||||
<script src="{{ url_for('static', filename='candle-aggregator.js') }}"></script>
|
<script src="{{ url_for('static', filename='candle-aggregator.js') }}"></script>
|
||||||
<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>
|
||||||
@ -115,89 +127,75 @@
|
|||||||
|
|
||||||
let baseCandleData1m = [];
|
let baseCandleData1m = [];
|
||||||
let displayedCandleData = [];
|
let displayedCandleData = [];
|
||||||
let currentCandle1m = null;
|
|
||||||
let manager;
|
let manager;
|
||||||
|
|
||||||
const timeframeSelect = document.getElementById('timeframe-select');
|
const timeframeSelect = document.getElementById('timeframe-select');
|
||||||
const candleTimerDiv = document.getElementById('candle-timer');
|
const candleTimerDiv = document.getElementById('candle-timer');
|
||||||
const chartTitle = document.getElementById('chart-title');
|
const chartTitle = document.getElementById('chart-title');
|
||||||
const analyzeButton = document.getElementById('analyzeButton');
|
const progressContainer = document.getElementById('progress-container');
|
||||||
const analysisResultDiv = document.getElementById('analysisResult');
|
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(true); // Initial load, fit content
|
||||||
|
setTimeout(() => { progressContainer.style.display = 'none'; }, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('trade', (trade) => {
|
socket.on('candle_update', (candle) => {
|
||||||
const price = parseFloat(trade.p);
|
candlestickSeries.update(candle);
|
||||||
const tradeTime = Math.floor(trade.T / 1000);
|
});
|
||||||
const candleTimestamp1m = tradeTime - (tradeTime % 60);
|
|
||||||
|
|
||||||
if (!currentCandle1m || candleTimestamp1m > currentCandle1m.time) {
|
socket.on('candle_closed', (closedCandle) => {
|
||||||
if (currentCandle1m) baseCandleData1m.push(currentCandle1m);
|
const lastBaseCandle = baseCandleData1m.length > 0 ? baseCandleData1m[baseCandleData1m.length - 1] : null;
|
||||||
currentCandle1m = { time: candleTimestamp1m, open: price, high: price, low: price, close: price };
|
if (lastBaseCandle && lastBaseCandle.time === closedCandle.time) {
|
||||||
|
baseCandleData1m[baseCandleData1m.length - 1] = closedCandle;
|
||||||
} else {
|
} else {
|
||||||
currentCandle1m.high = Math.max(currentCandle1m.high, price);
|
baseCandleData1m.push(closedCandle);
|
||||||
currentCandle1m.low = Math.min(currentCandle1m.low, price);
|
|
||||||
currentCandle1m.close = price;
|
|
||||||
}
|
}
|
||||||
|
updateChartForTimeframe(false); // Subsequent update, preserve zoom
|
||||||
const selectedInterval = parseInt(timeframeSelect.value, 10) * 60;
|
|
||||||
const displayedCandleTimestamp = tradeTime - (tradeTime % selectedInterval);
|
|
||||||
const lastDisplayedCandle = displayedCandleData[displayedCandleData.length - 1];
|
|
||||||
|
|
||||||
let candleForUpdate;
|
|
||||||
if (lastDisplayedCandle && displayedCandleTimestamp === lastDisplayedCandle.time) {
|
|
||||||
candleForUpdate = { ...lastDisplayedCandle };
|
|
||||||
candleForUpdate.high = Math.max(candleForUpdate.high, price);
|
|
||||||
candleForUpdate.low = Math.min(candleForUpdate.low, price);
|
|
||||||
candleForUpdate.close = price;
|
|
||||||
displayedCandleData[displayedCandleData.length - 1] = candleForUpdate;
|
|
||||||
} else if (!lastDisplayedCandle || displayedCandleTimestamp > lastDisplayedCandle.time) {
|
|
||||||
candleForUpdate = { time: displayedCandleTimestamp, open: price, high: price, low: price, close: price };
|
|
||||||
displayedCandleData.push(candleForUpdate);
|
|
||||||
|
|
||||||
// A new candle has started, so update the indicators.
|
|
||||||
manager.updateIndicatorsOnNewCandle(displayedCandleData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candleForUpdate) candlestickSeries.update(candleForUpdate);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateChartForTimeframe() {
|
function updateChartForTimeframe(isInitialLoad = false) {
|
||||||
const selectedIntervalMinutes = parseInt(timeframeSelect.value, 10);
|
const selectedIntervalMinutes = parseInt(timeframeSelect.value, 10);
|
||||||
if (baseCandleData1m.length === 0) return;
|
if (baseCandleData1m.length === 0) return;
|
||||||
|
|
||||||
|
const visibleRange = isInitialLoad ? null : chart.timeScale().getVisibleLogicalRange();
|
||||||
|
|
||||||
const newCandleData = aggregateCandles(baseCandleData1m, selectedIntervalMinutes);
|
const newCandleData = aggregateCandles(baseCandleData1m, selectedIntervalMinutes);
|
||||||
|
|
||||||
if (newCandleData.length > 0) {
|
if (newCandleData.length > 0) {
|
||||||
displayedCandleData = newCandleData;
|
displayedCandleData = newCandleData;
|
||||||
candlestickSeries.setData(displayedCandleData);
|
candlestickSeries.setData(displayedCandleData);
|
||||||
chartTitle.textContent = `{{ symbol }} Chart (${selectedIntervalMinutes}m)`;
|
chartTitle.textContent = `{{ symbol }} Chart (${selectedIntervalMinutes}m)`;
|
||||||
manager.recalculateAllAfterHistory(displayedCandleData);
|
manager.recalculateAllAfterHistory(baseCandleData1m, displayedCandleData);
|
||||||
chart.timeScale().fitContent();
|
|
||||||
|
if (visibleRange) {
|
||||||
|
chart.timeScale().setVisibleLogicalRange(visibleRange);
|
||||||
|
} else {
|
||||||
|
chart.timeScale().fitContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timeframeSelect.addEventListener('change', updateChartForTimeframe);
|
timeframeSelect.addEventListener('change', () => updateChartForTimeframe(true));
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const selectedIntervalSeconds = parseInt(timeframeSelect.value, 10) * 60;
|
const selectedIntervalSeconds = parseInt(timeframeSelect.value, 10) * 60;
|
||||||
|
|||||||
Reference in New Issue
Block a user