single hurst band works OK
This commit is contained in:
67
app.py
67
app.py
@ -12,7 +12,7 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
SYMBOL = 'ETHUSDT'
|
SYMBOL = 'ETHUSDT'
|
||||||
HISTORY_FILE = 'historical_data_1m.json' # Used as a cache to prevent re-downloading
|
HISTORY_FILE = 'historical_data_1m.json'
|
||||||
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"
|
||||||
|
|
||||||
@ -27,62 +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()
|
||||||
|
current_bar = {} # To track the currently forming 1-minute candle
|
||||||
|
|
||||||
# --- Historical Data Streaming ---
|
# --- Historical Data Streaming ---
|
||||||
def stream_historical_data(sid):
|
def stream_historical_data(sid):
|
||||||
"""
|
|
||||||
Fetches historical data in chunks and streams it to the client with progress updates.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
logging.info(f"Starting historical data stream for SID={sid}")
|
logging.info(f"Starting historical data stream for SID={sid}")
|
||||||
client = Client()
|
client = Client()
|
||||||
|
|
||||||
# Fetch the last 90 days of data in 6 chunks of 15 days each.
|
|
||||||
num_chunks = 6
|
num_chunks = 6
|
||||||
chunk_size_days = 15
|
chunk_size_days = 15
|
||||||
|
|
||||||
end_date = datetime.utcnow()
|
end_date = datetime.utcnow()
|
||||||
all_klines = []
|
all_klines = []
|
||||||
|
|
||||||
for i in range(num_chunks):
|
for i in range(num_chunks):
|
||||||
start_date = end_date - timedelta(days=chunk_size_days)
|
start_date = end_date - timedelta(days=chunk_size_days)
|
||||||
|
logging.info(f"Fetching chunk {i + 1}/{num_chunks} for SID={sid}")
|
||||||
logging.info(f"Fetching chunk {i + 1}/{num_chunks} ({start_date} to {end_date}) for SID={sid}")
|
|
||||||
new_klines = client.get_historical_klines(SYMBOL, Client.KLINE_INTERVAL_1MINUTE, str(start_date), str(end_date))
|
new_klines = client.get_historical_klines(SYMBOL, Client.KLINE_INTERVAL_1MINUTE, str(start_date), str(end_date))
|
||||||
|
|
||||||
if new_klines:
|
if new_klines:
|
||||||
all_klines.extend(new_klines)
|
all_klines.extend(new_klines)
|
||||||
|
socketio.emit('history_progress', {'progress': ((i + 1) / num_chunks) * 100}, to=sid)
|
||||||
progress_payload = {
|
|
||||||
'progress': ((i + 1) / num_chunks) * 100
|
|
||||||
}
|
|
||||||
socketio.emit('history_progress', progress_payload, to=sid)
|
|
||||||
|
|
||||||
end_date = start_date
|
end_date = start_date
|
||||||
socketio.sleep(0.05)
|
socketio.sleep(0.05)
|
||||||
|
|
||||||
seen = set()
|
seen = set()
|
||||||
unique_klines = []
|
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))]
|
||||||
for kline in sorted(all_klines, key=lambda x: x[0]):
|
|
||||||
kline_tuple = tuple(kline)
|
|
||||||
if kline_tuple not in seen:
|
|
||||||
unique_klines.append(kline)
|
|
||||||
seen.add(kline_tuple)
|
|
||||||
|
|
||||||
with open(HISTORY_FILE, 'w') as f:
|
with open(HISTORY_FILE, 'w') as f:
|
||||||
json.dump(unique_klines, f)
|
json.dump(unique_klines, f)
|
||||||
|
|
||||||
logging.info(f"Finished data stream for SID={sid}. Sending final payload of {len(unique_klines)} klines.")
|
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)
|
socketio.emit('history_finished', {'klines_1m': unique_klines}, to=sid)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in stream_historical_data for SID={sid}: {e}", exc_info=True)
|
logging.error(f"Error in stream_historical_data for SID={sid}: {e}", exc_info=True)
|
||||||
socketio.emit('history_error', {'message': str(e)}, to=sid)
|
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}...")
|
||||||
@ -90,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)
|
||||||
@ -109,17 +115,6 @@ def handle_connect():
|
|||||||
app_initialized = True
|
app_initialized = True
|
||||||
socketio.start_background_task(target=stream_historical_data, sid=request.sid)
|
socketio.start_background_task(target=stream_historical_data, sid=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('/')
|
||||||
def index():
|
def index():
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* 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>} displayedCandleData - A reference to the array with currently visible candles.
|
* @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, displayedCandleData) {
|
function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef) {
|
||||||
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: {} },
|
||||||
@ -96,9 +96,11 @@ function createIndicatorManager(chart, baseCandleData, displayedCandleData) {
|
|||||||
|
|
||||||
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) ? baseCandleDataRef : displayedCandleDataRef;
|
||||||
|
|
||||||
if (!slot || !slot.definition || candleDataForCalc.length === 0) return;
|
if (!slot || !slot.definition || candleDataForCalc.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
slot.series.forEach(s => chart.removeSeries(s));
|
slot.series.forEach(s => chart.removeSeries(s));
|
||||||
slot.series = [];
|
slot.series = [];
|
||||||
@ -131,21 +133,16 @@ function createIndicatorManager(chart, baseCandleData, displayedCandleData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function recalculateAllAfterHistory(newDisplayedCandleData) {
|
function recalculateAllAfterHistory(baseData, displayedData) {
|
||||||
displayedCandleData = newDisplayedCandleData;
|
baseCandleDataRef = baseData;
|
||||||
|
displayedCandleDataRef = displayedData;
|
||||||
indicatorSlots.forEach(slot => {
|
indicatorSlots.forEach(slot => {
|
||||||
if (slot.definition) updateIndicator(slot.id);
|
if (slot.definition) updateIndicator(slot.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is not currently used with the new model but is kept for potential future use.
|
|
||||||
function updateIndicatorsOnNewCandle(newCandle) {
|
|
||||||
// Real-time updates would go here.
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
populateDropdowns,
|
populateDropdowns,
|
||||||
recalculateAllAfterHistory,
|
recalculateAllAfterHistory,
|
||||||
updateIndicatorsOnNewCandle
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -127,14 +127,11 @@
|
|||||||
|
|
||||||
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 analysisResultDiv = document.getElementById('analysisResult');
|
|
||||||
const progressContainer = document.getElementById('progress-container');
|
const progressContainer = document.getElementById('progress-container');
|
||||||
const progressBar = document.querySelector('.progress-bar');
|
const progressBar = document.querySelector('.progress-bar');
|
||||||
|
|
||||||
@ -158,64 +155,50 @@
|
|||||||
low: parseFloat(k[3]), close: parseFloat(k[4])
|
low: parseFloat(k[3]), close: parseFloat(k[4])
|
||||||
}));
|
}));
|
||||||
|
|
||||||
updateChartForTimeframe();
|
updateChartForTimeframe(true); // Initial load, fit content
|
||||||
|
|
||||||
setTimeout(() => { progressContainer.style.display = 'none'; }, 500);
|
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) {
|
|
||||||
if (currentCandle1m) {
|
|
||||||
baseCandleData1m.push(currentCandle1m);
|
|
||||||
manager.updateIndicatorsOnNewCandle(currentCandle1m);
|
|
||||||
}
|
|
||||||
currentCandle1m = { time: candleTimestamp1m, open: price, high: price, low: price, close: price };
|
|
||||||
} else {
|
|
||||||
currentCandle1m.high = Math.max(currentCandle1m.high, price);
|
|
||||||
currentCandle1m.low = Math.min(currentCandle1m.low, price);
|
|
||||||
currentCandle1m.close = price;
|
|
||||||
}
|
|
||||||
|
|
||||||
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, high: Math.max(lastDisplayedCandle.high, price), low: Math.min(lastDisplayedCandle.low, price), close: price };
|
|
||||||
displayedCandleData[displayedCandleData.length - 1] = candleForUpdate;
|
|
||||||
} else {
|
|
||||||
candleForUpdate = { time: displayedCandleTimestamp, open: price, high: price, low: price, close: price };
|
|
||||||
displayedCandleData.push(candleForUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candleForUpdate) candlestickSeries.update(candleForUpdate);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateChartForTimeframe() {
|
socket.on('candle_closed', (closedCandle) => {
|
||||||
|
const lastBaseCandle = baseCandleData1m.length > 0 ? baseCandleData1m[baseCandleData1m.length - 1] : null;
|
||||||
|
if (lastBaseCandle && lastBaseCandle.time === closedCandle.time) {
|
||||||
|
baseCandleData1m[baseCandleData1m.length - 1] = closedCandle;
|
||||||
|
} else {
|
||||||
|
baseCandleData1m.push(closedCandle);
|
||||||
|
}
|
||||||
|
updateChartForTimeframe(false); // Subsequent update, preserve zoom
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (visibleRange) {
|
||||||
|
chart.timeScale().setVisibleLogicalRange(visibleRange);
|
||||||
|
} else {
|
||||||
chart.timeScale().fitContent();
|
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;
|
||||||
// **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);
|
||||||
|
|||||||
Reference in New Issue
Block a user