315 lines
14 KiB
Python
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)
|