import json import logging from flask import Flask, render_template, request, redirect, jsonify, session, flash from flask_cors import CORS from flask_sock import Sock from email_validator import validate_email, EmailNotValidError # Handles all updates and requests for locally stored data. import config from BrighterTrades import BrighterTrades # Set up logging logging.basicConfig(level=logging.DEBUG) # Create a BrighterTrades object. This the main application that maintains access to the server, local storage, # and manages objects that process trade data. brighter_trades = BrighterTrades() # Create a Flask object named app that serves the html. app = Flask(__name__) # Create a socket in order to receive requests. sock = Sock(app) # Set server configuration globals. CORS_HEADERS = 'Content-Type' # Set the app directly with the globals. app.config.from_object(__name__) app.secret_key = '1_BAD_secrete_KEY_is_not_2' # Enable cross-origin resource sharing for specific endpoints cors = CORS(app, supports_credentials=True, resources={ r"/api/history": {"origins": ["http://127.0.0.1:5000", "http://localhost:5000"]}, r"/api/indicator_init": {"origins": ["http://127.0.0.1:5000", "http://localhost:5000"]} }, allow_headers=['Content-Type']) # Change from headers to allow_headers @app.after_request def add_cors_headers(response): response.headers['Access-Control-Allow-Credentials'] = 'true' return response @app.route('/') # @cross_origin(supports_credentials=True) def index(): """ Fetches data from brighter_trades and inject it into an HTML template. Renders the html template and serves the web application. """ # Clear the session to simulate a new visitor session.clear() try: # Log the user in. user_name = brighter_trades.config.users.load_or_create_user(username=session.get('user')) except ValueError as e: if str(e) != 'GuestLimitExceeded!': raise else: flash('The system is too busy for another user.') return # Ensure client session is assigned a user_name. session['user'] = user_name print('[SERVING INDEX] (USERNAME):', user_name) default_exchange = 'binance' default_keys = None # No keys needed for public market data # Ensure that a valid connection with an exchange exists result = brighter_trades.connect_user_to_exchange(user_name=user_name, default_exchange=default_exchange, default_keys=default_keys) if not result: raise ValueError("Couldn't connect to the default exchange.") # A dict of data required to build the html of the user app. # Dynamic content like options and titles and balances to display. rendered_data = brighter_trades.get_rendered_data(user_name) # Jsom initialization data to be passed into the web application. js_data = brighter_trades.get_js_init_data(user_name) # serve the landing page. return render_template('index.html', title=rendered_data['title'], user_name=user_name, my_balances=rendered_data['my_balances'], symbols=rendered_data['symbols'], intervals=rendered_data['intervals'], interval_state=rendered_data['chart_interval'], exchanges=rendered_data['available_exchanges'], cond_exchanges=rendered_data['connected_exchanges'], confd_exchanges=rendered_data['configured_exchanges'], selected_exchange=rendered_data['selected_exchange'], indicator_types=rendered_data['indicator_types'], indicator_list=rendered_data['indicator_list'], checked=rendered_data['enabled_indicators'], ma_vals=rendered_data['ma_vals'], js_data=js_data, active_trades=rendered_data['active_trades'], open_orders=rendered_data['open_orders']) @sock.route('/ws') def ws(socket_conn): """ Open a websocket to handle two-way communication with UI without browser refreshes. """ def json_msg_received(msg_obj): """ Handle incoming json msg's. """ # Validate input if 'message_type' not in msg_obj: return msg_type, msg_data = msg_obj['message_type'], msg_obj['data'] print(f'\n[MSG_RECEIVED]\nMSG Type:{msg_type}\n{msg_data}') resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data) socket_conn.send(json.dumps(resp)) return # The rendered page connects to the exchange_interface and relays the candle data back here # this socket also handles data and processing requests while True: msg = socket_conn.receive() if msg: # If in json format the message gets converted into a dictionary # otherwise it is handled as a status signal from the client try: json_msg = json.loads(msg) json_msg_received(json_msg) except json.JSONDecodeError: print(f'Msg received from client: {msg}') @app.route('/settings', methods=['POST']) def settings(): """ This route is used to change settings that require a browser refresh. Options are: Data source options for the chart display: timeframe: For setting the time frame the of the viewport. trading_pair: For setting the market displayed in the viewport. exchange_name: For setting the exchange_name displayed in the viewport. Controls of the indicators: new_indicator: Creates a new indicator and stores it. edit_indicator: Edits the properties of specific indicators. toggle_indicator: Enables or disables display of indicators in the viewport. :return: None - Redirects to index. """ # Request the user_name from the client session. if not (user_name := session.get('user')): return redirect('/') # Return if the user is not logged in. if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'): return redirect('/') # Get the setting that the web application wants to change. if not (setting := request.form.get('setting')): return redirect('/') # Change the setting. brighter_trades.adjust_setting(user_name=user_name, setting=setting, params=request.form) return redirect('/') @app.route('/api/history', methods=['POST', 'GET']) def history(): """ Fetches the candle history of a specific trading pair and timeframe. Currently, set to last 1000 candles. :return: json - price history """ try: data = request.get_json() if not data: raise ValueError("No JSON data received") username = data.get('user_name') # Return if the user is not logged in. if not username: # redirect('/') return jsonify({'error': 'User not logged in.'}), 401 print('[RECEIVED PRICE HISTORY REQUEST] - user', username) # Get a dictionary of chart attributes from the user data indexed by user_name. chart_view = brighter_trades.get_user_info(user_name=username, info='Chart View') payload = brighter_trades.get_market_info( info='Candle History', num_records=1000, chart_view=chart_view, user_name=username) print('[SENDING PRICE HISTORY TO CLIENT]', payload.tail(2)) return jsonify(payload.to_dict('records')), 200 except Exception as e: print(f'Error in history endpoint: {e}') return jsonify({'error': str(e)}), 500 # @app.route('/saved_data') # def saved_data(): # """Returns the saved settings for the indicators""" # # Request the user_name from the client session. # if not (user_name := session.get('user')): # return redirect('/') # # # Return if the user is not logged in. # if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'): # return redirect('/') # # return jsonify(brighter_trades.indicators.indicator_list) @app.route('/signup') def signup(): return render_template('sign_up.html', title='title') @app.route('/signout') def signout(): if not (user_name := session.get('user')): return redirect('/') if brighter_trades.log_user_in_out(user_name=user_name, cmd='logout'): # If the user was logged out successfully delete the session var. del session['user'] return redirect('/') @app.route('/login', methods=['POST']) def login(): # Get the user_name and password from the form data username = request.form.get('user_name') password = request.form.get('password') # Validate the input if not username or not password: flash('Please provide both user_name and password.') return redirect('/') # Attempt to log in the user success = brighter_trades.log_user_in_out(user_name=username, cmd='login', password=password) if success: # Store the user_name in the session session['user'] = username flash('Login successful!') else: flash('Invalid user_name or password.') return redirect('/') @app.route('/signup_submit', methods=['POST']) def signup_submit(): email = request.form.get('email') username = request.form.get('user_name') password = request.form.get('password') # Validate email format try: validate_email(email) except EmailNotValidError as e: flash(message=f"Invalid email format: {e}") return None # Validate user_name and password if not username or not password: flash(message="Missing user_name or password") return None # Create a new user success = brighter_trades.create_new_user(email=email, username=username, password=password) if success: session['user'] = username return redirect('/') else: flash(message="An error has occurred during the signup process.") return None @app.route('/api/indicator_init', methods=['POST', 'GET']) def indicator_init(): """ Initializes the indicators and returns the data for a given symbol and timeframe. """ data = request.get_json() username = data.get('user_name') if not username: return jsonify({'error': 'Invalid user name.'}), 400 # Check if the user is logged in if not brighter_trades.get_user_info(user_name=username, info='Is logged in?'): return jsonify({'error': 'User is not logged in.'}), 401 # Get the chart view for the user chart_view = brighter_trades.get_user_info(user_name=username, info='Chart View') # Get the indicator data source = {'user_name': username, 'market': chart_view} data = brighter_trades.get_indicator_data(user_name=username, source=source, start_ts=None, num_results=1000) # indicators={'EMA 5': {'visible': true, 'type': 'EMA', 'color': 'red' },'vol': {'visible': true, 'type': 'Volume'},'New Indicator': {'visible': true, 'type': 'nothing'}} return jsonify(data), 200 if __name__ == '__main__': app.run(debug=False, use_reloader=False)