import json # Flask is a lightweight html server used to render the UI. from flask import Flask, render_template, request, redirect, jsonify from flask_cors import cross_origin from binance.enums import * from flask_sock import Sock # Handles all updates and requests for locally stored data. from data import BrighterData # Create a Flask object named interface that serves the html. interface = Flask(__name__) # Create a socket in order to receive requests. sock = Sock(interface) # Create a BrighterData object. This the main application that maintains access to the server, local storage, # and manages objects that process trade data. app_data = BrighterData() # TODO: The cors object had something to do with an error I was getting while # receiving json. It may not be needed anymore. # interface.config['CORS_HEADERS'] = 'Content-Type' # cors = CORS(interface, resources={r"*": {"origins": "*"}}) @interface.route('/') def index(): # Generate and return a landing page for the web application. # Fetch data from app_data and inject it into an HTML template. # Render the template and serve it upon request. rendered_data = app_data.get_rendered_data() js_data = app_data.get_js_init_data() return render_template('index.html', title=rendered_data['title'], my_balances=rendered_data['my_balances'], symbols=rendered_data['symbols'], intervals=rendered_data['intervals'], interval_state=rendered_data['chart_interval'], 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) @sock.route('/ws') def ws(sock): # TODO: Note I started using a socket to receive messages from the UI rather then the # Flask interfaces because it was getting complicated to return data to the UI objects. # I was getting error messages regarding headers while transferring json objects # and found it problematic dealing with browser refreshes. # Define a websocket to handle request originating from the UI. # Handle incoming json msg's. def json_msg_received(msg_obj): if 'message_type' not in msg_obj: return if msg_obj['message_type'] == 'candle_data': # If we received candle data. Send it to the BrighterData_obj # and forward any returned data to the client. r_data = app_data.received_cdata(msg_obj['data']) if r_data: resp = { "reply": "updates", "data": r_data } sock.send(json.dumps(resp)) if msg_obj['message_type'] == 'request': # If a request for data is received fetch it from the appropriate object # and return. if msg_obj['data'] == 'signals': signals = app_data.get_signals() if signals: resp = { "reply": "signals", "data": signals } resp = json.dumps(resp) sock.send(resp) elif msg_obj['data'] == 'strategies': strategies = app_data.get_strategies() if strategies: resp = { "reply": "strategies", "data": strategies } resp = json.dumps(resp) sock.send(resp) elif msg_obj['data'] == 'trades': trades = app_data.get_trades() if trades: resp = { "reply": "trades", "data": trades } resp = json.dumps(resp) sock.send(resp) else: print('Warning: Unhandled request!') print(msg_obj['data']) # If the message is a command. # Pass the command and data on to app_data to process. if msg_obj['message_type'] == 'delete_signal': app_data.delete_signal(msg_obj['data']) if msg_obj['message_type'] == 'delete_strategy': app_data.delete_strategy(msg_obj['data']) if msg_obj['message_type'] == 'new_signal': # Send the data to the BrighterData object. # and forward any returned data to the client. r_data = app_data.received_new_signal(msg_obj['data']) if r_data: resp = { "reply": "signal_created", "data": r_data } resp = json.dumps(resp) sock.send(resp) if msg_obj['message_type'] == 'new_strategy': # Send the data to the BrighterData_obj # and forward any returned data to the client. r_data = app_data.received_new_strategy(msg_obj['data']) if r_data: resp = { "reply": "strategy_created", "data": r_data } resp = json.dumps(resp) sock.send(resp) if msg_obj['message_type'] == 'new_trade': # Send the data to the BrighterData_obj # and forward any returned data to the client. r_data = app_data.received_new_trade(msg_obj['data']) if r_data: resp = { "reply": "trade_created", "data": r_data } resp = json.dumps(resp) sock.send(resp) # If the message is a reply log the response to the terminal. if msg_obj['message_type'] == 'reply': print(f"\napp.py:Received reply: {msg_obj['rep']}") return # The rendered page connects to the exchange and relays the candle data back here # this socket also handles data and processing requests while True: msg = sock.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: msg_obj = json.loads(msg) json_msg_received(msg_obj) except json.JSONDecodeError: print(f'Msg received from client: {msg}') # @interface.route('/trade', methods=['POST']) # @cross_origin(origin='localhost', headers=['Content- Type', 'Authorization']) # def trade(): # # todo: This interface method is a straight forward approach to communicating from the frontend to backend # # it only require an html form on the UI side. It's difficult to return a response without refreshing the UI. # # I may be missing a solution that doesn't require rendering a hidden frame in the UI and complicating the code # # further. I am abandoning this approach for socket method above. Although I am uncomfortable with the loop # # the above solution requires. The server probably has already implemented such a loop so it seems inefficient. # # Also I would like to implement data processing loops on the server side that may compete for resources. # # One solution is to create another interface /trade_response. This would require a buffer so nothing gets # # missed or overwritten. /trade_response would be polled by the UI at set intervals. Not sure if it # # would decrease overhead to the server. I'm keeping this here for future consideration. # def fetch(attr): # # Verify and validate input data. # if attr in request.form and request.form[attr] != '': # return request.form[attr] # else: # return None # # Forward the request to data. # status, result = app_data.received_new_trade(symbol=fetch('symbol'), price=fetch('price'), # side=fetch('side'), order_type=fetch('orderType'), # quantity=fetch('quantity')) # # Log any error to the terminal. # if status == 'Error': # print(f'\napp.py:trade() - Error placing the trade: {result}') # # Log order to terminal. # print(f"\napp.py:trade() - Trade order received: symbol={fetch('symbol')}," # f" side={fetch('side')}, type={fetch('orderType')}, quantity={fetch('quantity')},price={fetch('price')}") # # return redirect('/') @interface.route('/settings', methods=['POST']) @cross_origin(origin='localhost', headers=['Content- Type', 'Authorization']) def settings(): """ This route is used to change setting of the trading app that require a browser refresh. Options are: interval: For setting the time frame the app is trading in. trading_pair: The pair being traded. toggle_indicator: enables or disables indicators edit_indicator: edits the properties of specific indicators. new_indicator: Creates a new indicator and stores it. :return: redirects the browser to the index / """ setting = request.form['setting'] if setting == 'interval': interval_state = request.form['timeframe'] app_data.config.set_chart_interval(interval_state) elif setting == 'trading_pair': trading_pair = request.form['symbol'] app_data.config.set_trading_pair(trading_pair) elif setting == 'toggle_indicator': # Create a list of all the enabled indicators. enabled_indicators = [] for i in request.form: if request.form[i] == 'indicator': enabled_indicators.append(i) # Set visibility for all indicators according to for indctr in app_data.indicators.indicator_list: if indctr in enabled_indicators: app_data.indicators.indicator_list[indctr]['visible'] = True else: app_data.indicators.indicator_list[indctr]['visible'] = False # Redirect without reloading history app_data.config.config_and_states('save') return redirect('/') elif setting == 'edit_indicator': if 'submit' in request.form: # Get the name of the indicator indicator = request.form['submit'] # Drop the name and action from our received data attributes = dict(list(request.form.items())[2:]) # All the numbers are string now so turn them back to (int) for a in attributes: if attributes[a].isdigit(): attributes[a] = int(attributes[a]) # if visible is unchecked it doesn't get sent by the form if 'visible' not in attributes: attributes.update({'visible': False}) # Set the data in indicators according to app_data.indicators.indicator_list[indicator] = attributes if 'delete' in request.form: indicator = request.form['delete'] # This will delete in both indicators and config. app_data.indicators.delete_indicator(indicator) # Redirect without reloading history app_data.config.config_and_states('save') return redirect('/') elif setting == 'new_indicator': if 'newi_name' in request.form: indcr = request.form['newi_name'] indtyp = request.form['newi_type'] properties = {} if request.form['new_prop_obj']: list_of_dic = json.loads(request.form['new_prop_obj']) # All the numbers are string now so turn them back to (int) properties = {} for prop in list_of_dic: # Get the key for this object key = next(iter(prop)) # access the value of this object value = prop[key] if value.isdigit(): value = int(value) properties[key] = value # Should create in indicators and update the list in config. app_data.indicators.create_indicator(name=indcr, itype=indtyp, properties=properties) else: print('ERROR SETTING VALUE') print(f'The string received by the server was: /n{request.form}') app_data.config.config_and_states('save') app_data.candles.set_candle_history() return redirect('/') @interface.route('/history') @cross_origin(origin='localhost', headers=['Content- Type', 'Authorization']) def history(): # Returns the candle history of a specific trading pair and interval. # Currently, set to last 1000 candles. symbol = app_data.config.trading_pair interval = app_data.config.chart_interval return jsonify(app_data.candles.get_candle_history(symbol, interval, 1000)) @interface.route('/saved_data') @cross_origin(origin='localhost', headers=['Content- Type', 'Authorization']) def saved_data(): # Returns the saved settings for the indicators return jsonify(app_data.indicators.indicator_list) @interface.route('/indicator_init') @cross_origin(origin='localhost', headers=['Content- Type', 'Authorization']) def indicator_init(): # Initializes the indicators and returns the data for a given symbol and interval. symbol = app_data.config.trading_pair interval = app_data.config.chart_interval d = app_data.indicators.get_indicator_data(symbol, interval, 800) return jsonify(d)