""" ProTrade Charting Platform - Flask Backend Handles API endpoints and WebSocket connections for real-time data. """ import os import sys import json import time import threading from datetime import datetime from flask import Flask, send_from_directory, jsonify, request from flask_cors import CORS from flask_socketio import SocketIO, emit # Add current directory to path for imports sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from data_feeds import data_feed_manager from indicators import calculate_all_indicators, calculate_volume_profile, calculate_fair_value_gaps import pandas as pd # Create Flask app - static folder is relative to backend/ folder import os as os_module static_path = os_module.path.join(os_module.path.dirname(os_module.path.abspath(__file__)), '..', 'dist') app = Flask(__name__, static_folder=static_path, static_url_path='') CORS(app) # SocketIO with eventlet for production socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet', ping_timeout=60, ping_interval=25) # Client subscription tracking client_subscriptions = {} @app.route('/') def index(): """Serve the main React app""" return send_from_directory(app.static_folder, 'index.html') @app.route('/') def serve_static(path): """Serve static files""" if os.path.exists(os.path.join(app.static_folder, path)): return send_from_directory(app.static_folder, path) return send_from_directory(app.static_folder, 'index.html') @app.route('/api/search') def search_symbols(): """Search for symbols across markets""" query = request.args.get('q', '') market_type = request.args.get('market', 'us_stock') if not query: return jsonify([]) results = data_feed_manager.search_symbols(query, market_type) return jsonify(results) @app.route('/api/symbol-info') def symbol_info(): """Get detailed symbol information""" symbol = request.args.get('symbol', '') market_type = request.args.get('market', 'us_stock') if not symbol: return jsonify({'error': 'Symbol required'}), 400 info = data_feed_manager.get_symbol_info(symbol, market_type) return jsonify(info) @app.route('/api/historical') def get_historical(): """Get historical OHLCV data""" symbol = request.args.get('symbol', '') market_type = request.args.get('market', 'us_stock') timeframe = request.args.get('timeframe', '1d') if not symbol: return jsonify({'error': 'Symbol required'}), 400 if market_type == 'crypto': candles = data_feed_manager.fetch_hyperliquid_data(symbol, timeframe) else: candles = data_feed_manager.fetch_yfinance_data(symbol, market_type, timeframe) data = [{ 'time': c.time, 'open': c.open, 'high': c.high, 'low': c.low, 'close': c.close, 'volume': c.volume } for c in candles] return jsonify(data) @app.route('/api/indicators') def get_indicators(): """Calculate technical indicators""" symbol = request.args.get('symbol', '') market_type = request.args.get('market', 'us_stock') timeframe = request.args.get('timeframe', '1d') if not symbol: return jsonify({'error': 'Symbol required'}), 400 # Parse indicator config from request indicators_config = request.args.get('indicators', '[]') try: indicators_config = json.loads(indicators_config) except: indicators_config = [] if market_type == 'crypto': candles = data_feed_manager.fetch_hyperliquid_data(symbol, timeframe) else: candles = data_feed_manager.fetch_yfinance_data(symbol, market_type, timeframe) if not candles: return jsonify({'error': 'No data available'}), 404 # Convert to DataFrame df = pd.DataFrame([{ 'open': c.open, 'high': c.high, 'low': c.low, 'close': c.close, 'volume': c.volume } for c in candles]) df.index = pd.to_datetime([c.time * 1000 for c in candles], unit='ms') # Calculate indicators results = calculate_all_indicators(df, {'indicators': indicators_config}) # Add timestamps for key in results: if key in ['macd']: for k in results[key]: results[key][k] = [None if pd.isna(v) else v for v in results[key][k]] elif key not in ['volume_profile', 'fvg']: if isinstance(results[key], list): results[key] = [None if pd.isna(v) else v for v in results[key]] return jsonify({ 'timestamps': [c.time for c in candles], 'indicators': results }) @app.route('/api/volume-profile') def get_volume_profile(): """Get Volume Profile analysis""" symbol = request.args.get('symbol', '') market_type = request.args.get('market', 'us_stock') timeframe = request.args.get('timeframe', '1d') if not symbol: return jsonify({'error': 'Symbol required'}), 400 if market_type == 'crypto': candles = data_feed_manager.fetch_hyperliquid_data(symbol, timeframe) else: candles = data_feed_manager.fetch_yfinance_data(symbol, market_type, timeframe) if not candles: return jsonify({'error': 'No data available'}), 404 df = pd.DataFrame([{ 'open': c.open, 'high': c.high, 'low': c.low, 'close': c.close, 'volume': c.volume } for c in candles]) vp = calculate_volume_profile(df) return jsonify(vp) @app.route('/api/fvg') def get_fvg(): """Get Fair Value Gaps""" symbol = request.args.get('symbol', '') market_type = request.args.get('market', 'us_stock') timeframe = request.args.get('timeframe', '1d') if not symbol: return jsonify({'error': 'Symbol required'}), 400 if market_type == 'crypto': candles = data_feed_manager.fetch_hyperliquid_data(symbol, timeframe) else: candles = data_feed_manager.fetch_yfinance_data(symbol, market_type, timeframe) if not candles: return jsonify({'error': 'No data available'}), 404 df = pd.DataFrame([{ 'open': c.open, 'high': c.high, 'low': c.low, 'close': c.close, 'volume': c.volume } for c in candles]) df.index = pd.to_datetime([c.time * 1000 for c in candles], unit='ms') fvg = calculate_fair_value_gaps(df) return jsonify(fvg) @app.route('/api/market-status') def market_status(): """Get market status for a symbol""" symbol = request.args.get('symbol', '') market_type = request.args.get('market', 'us_stock') if market_type == 'crypto': return jsonify({'status': 'open', 'message': '24/7 Market'}) # Check US market hours (9:30 AM - 4:00 PM ET, Mon-Fri) now = datetime.now() weekday = now.weekday() if weekday >= 5: # Weekend return jsonify({'status': 'closed', 'message': 'Market Closed - Weekend'}) # Rough check - not accounting for timezone differences hour = now.hour if 14 <= hour <= 21: # Approximate US market hours in UTC return jsonify({'status': 'open', 'message': 'Market Open'}) else: return jsonify({'status': 'closed', 'message': 'Market Closed'}) # Socket.IO Events @socketio.on('connect') def handle_connect(): """Handle client connection""" sid = request.sid client_subscriptions[sid] = {} emit('connected', {'status': 'connected', 'sid': sid}) @socketio.on('disconnect') def handle_disconnect(): """Handle client disconnection""" sid = request.sid if sid in client_subscriptions: for key in client_subscriptions[sid]: data_feed_manager.unsubscribe(key) del client_subscriptions[sid] @socketio.on('subscribe') def handle_subscribe(data): """Subscribe to real-time data feed""" sid = request.sid symbol = data.get('symbol', '') market_type = data.get('marketType', 'us_stock') timeframe = data.get('timeframe', '1d') chart_id = data.get('chartId', 'default') if not symbol: emit('error', {'message': 'Symbol required'}) return def on_data(candles): """Callback for new data""" formatted = [{ 'time': c.time, 'open': c.open, 'high': c.high, 'low': c.low, 'close': c.close, 'volume': c.volume } for c in candles] socketio.emit('data_update', { 'chartId': chart_id, 'symbol': symbol, 'timeframe': timeframe, 'data': formatted }, room=sid) key = data_feed_manager.subscribe(symbol, market_type, timeframe, on_data) if sid not in client_subscriptions: client_subscriptions[sid] = {} client_subscriptions[sid][chart_id] = key emit('subscribed', { 'chartId': chart_id, 'symbol': symbol, 'timeframe': timeframe, 'dataPoints': len(data_feed_manager.get_latest_data(key)) }) @socketio.on('unsubscribe') def handle_unsubscribe(data): """Unsubscribe from a data feed""" sid = request.sid chart_id = data.get('chartId', 'default') if sid in client_subscriptions and chart_id in client_subscriptions[sid]: key = client_subscriptions[sid][chart_id] data_feed_manager.unsubscribe(key) del client_subscriptions[sid][chart_id] emit('unsubscribed', {'chartId': chart_id}) def start_polling(): """Start background polling for data updates""" while True: try: with data_feed_manager.lock: subs = list(data_feed_manager.subscriptions.items()) for key, sub in subs: if sub.market_type == 'crypto': # Refresh crypto data new_data = data_feed_manager.fetch_hyperliquid_data( sub.symbol, sub.timeframe ) else: # Refresh stock data new_data = data_feed_manager.fetch_yfinance_data( sub.symbol, sub.market_type, sub.timeframe ) if new_data and len(new_data) > 0: data_feed_manager.update_data(key, new_data) except Exception as e: print(f"Polling error: {e}") time.sleep(10) # Poll every 10 seconds # Start background polling thread polling_thread = threading.Thread(target=start_polling, daemon=True) polling_thread.start() if __name__ == '__main__': port = int(os.environ.get('PORT', 7860)) socketio.run(app, host='0.0.0.0', port=port, debug=False)