brighter-trading/app.py

315 lines
14 KiB
Python

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 <enabled_indicators>
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 <attributes>
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)