diff --git a/.idea/BrighterTrading.iml b/.idea/BrighterTrading.iml
index 45f5450..a98d843 100644
--- a/.idea/BrighterTrading.iml
+++ b/.idea/BrighterTrading.iml
@@ -11,4 +11,7 @@
+
+
+
\ No newline at end of file
diff --git a/Configuration.py b/Configuration.py
index 5b6a78a..9a88aaa 100644
--- a/Configuration.py
+++ b/Configuration.py
@@ -31,21 +31,30 @@ class Configuration:
# list of strategies in config.
self.strategies_list = []
+ # list of trades.
+ self.trades = []
+
# The data that will be saved and loaded from file .
self.saved_data = None
- def new_strategy(self, data):
- # The strategies_list is modified by reference in strategies the loaded in config_and_states('save').
- # Save it to file.
- self.config_and_states('save')
+ # Load any saved data from file
+ self.config_and_states('load')
- def new_signal(self, data):
- # Create a new signal.
- self.saved_data['signals'].append(data)
+ def update_data(self, data_type, data):
+ # Replace current list of data sets with an updated list.
+ if data_type == 'strategies':
+ self.strategies_list = data
+ elif data_type == 'signals':
+ self.signals_list = data
+ elif data_type == 'trades':
+ self.trades = data
+ else:
+ raise ValueError(f'Configuration: update_data(): Unsupported data_type: {data_type}')
# Save it to file.
self.config_and_states('save')
def remove(self, what, name):
+ # Removes by name an item from a list in saved data.
print(f'removing {what}:{name}')
for obj in self.saved_data[what]:
if obj['name'] == name:
@@ -54,6 +63,12 @@ class Configuration:
# Save it to file.
self.config_and_states('save')
+ def set_chart_interval(self, interval):
+ self.chart_interval = interval
+
+ def set_trading_pair(self, symbol):
+ self.trading_pair = symbol
+
def config_and_states(self, cmd):
"""Loads or saves configurable data to the file set in self.config_FN"""
@@ -62,16 +77,29 @@ class Configuration:
'indicator_list': self.indicator_list,
'config': {'chart_interval': self.chart_interval, 'trading_pair': self.trading_pair},
'signals': self.signals_list,
- 'strategies': self.strategies_list
+ 'strategies': self.strategies_list,
+ 'trades': self.trades
}
def set_loaded_values():
# Sets the values in the saved_data object.
- self.indicator_list = self.saved_data['indicator_list']
- self.chart_interval = self.saved_data['config']['chart_interval']
- self.trading_pair = self.saved_data['config']['trading_pair']
- self.signals_list = self.saved_data['signals']
- self.strategies_list = self.saved_data['strategies']
+ if 'indicator_list' in self.saved_data:
+ self.indicator_list = self.saved_data['indicator_list']
+
+ if 'chart_interval' in self.saved_data['config']:
+ self.chart_interval = self.saved_data['config']['chart_interval']
+
+ if 'trading_pair' in self.saved_data['config']:
+ self.trading_pair = self.saved_data['config']['trading_pair']
+
+ if 'signals' in self.saved_data:
+ self.signals_list = self.saved_data['signals']
+
+ if 'strategies' in self.saved_data:
+ self.strategies_list = self.saved_data['strategies']
+
+ if 'trades' in self.saved_data:
+ self.trades = self.saved_data['trades']
def load_configuration(filepath):
"""load file data"""
@@ -88,13 +116,15 @@ class Configuration:
# If load_configuration() finds a file it overwrites
# the saved_data object otherwise it creates a new file
# with the defaults contained in saved_data>
+
+ # If file exist load the values.
try:
- # If file exist load the values.
self.saved_data = load_configuration(self.config_FN)
set_loaded_values()
+ # If file doesn't exist create a file and save the default values.
except IOError:
- # If file doesn't exist create a file and save the default values.
save_configuration(self.config_FN, self.saved_data)
+
elif cmd == 'save':
try:
# Write saved_data to the file.
diff --git a/Signals.py b/Signals.py
index 01e8dc9..bdec03b 100644
--- a/Signals.py
+++ b/Signals.py
@@ -1,3 +1,4 @@
+import json
from dataclasses import dataclass
@@ -51,19 +52,14 @@ class Signal:
class Signals:
- def __init__(self):
- self.signals = []
- # self.set_signals_defaults()
+ def __init__(self, loaded_signals=None):
- def set_signals_defaults(self):
- """These defaults are loaded if the config file is not found."""
- sigs = self.get_signals_defaults()
- for sig in sigs:
- self.signals.append(Signal(name=sig['name'], source1=sig['source1'],
- prop1=sig['prop1'], operator=sig['operator'],
- source2=sig['source2'], prop2=sig['prop2'],
- state=sig['state']))
- return
+ # list of Signal objects.
+ self.signals = []
+
+ # Initialize signals with loaded data.
+ if loaded_signals is not None:
+ self.create_signal_from_dic(loaded_signals)
@staticmethod
def get_signals_defaults():
@@ -85,15 +81,41 @@ class Signals:
"value2": None, "range": None}
return [s1, s2, s3]
- def init_loaded_signals(self, signals_list):
- for sig in signals_list:
- self.signals.append(Signal(name=sig['name'], source1=sig['source1'],
- prop1=sig['prop1'], operator=sig['operator'],
- source2=sig['source2'], prop2=sig['prop2'],
- state=sig['state']))
+ def create_signal_from_dic(self, signals_list=None):
+ """
+ :param signals_list: list of dict
+ :return True: on success.
+ Create and store signal objects from list of dictionaries.
+ """
- def get_signals(self):
- return self.signals
+ # If no signals were provided used a default list.
+ if signals_list is None:
+ signals_list = self.get_signals_defaults()
+ # Loop through the provided list, unpack the dictionaries, create and store the signal objects.
+ for sig in signals_list:
+ self.new_signal(sig)
+ return True
+
+ def get_signals(self, form):
+ # Return a python object of all the signals stored in this instance.
+ if form == 'obj':
+ return self.signals
+ # Return a JSON object of all the signals stored in this instance.
+ elif form == 'json':
+ sigs = self.signals
+ json_str = []
+ for sig in sigs:
+ # TODO: note - Explore why I had to treat signals and strategies different here.
+ json_str.append(json.dumps(sig.__dict__))
+ return json_str
+ # Return a dictionary object of all the signals stored in this instance.
+ elif form == 'dict':
+ sigs = self.signals
+ s_list = []
+ for sig in sigs:
+ dic = sig.__dict__
+ s_list.append(dic)
+ return s_list
def get_signal_by_name(self, name):
for signal in self.signals:
diff --git a/Strategies.py b/Strategies.py
index 846150b..f505802 100644
--- a/Strategies.py
+++ b/Strategies.py
@@ -1,25 +1,200 @@
import json
+class Strategy:
+ def __init__(self, **args):
+ """
+ :param args: An object containing key_value pairs representing strategy attributes.
+ Strategy format is defined in strategies.js
+ """
+ self.current_value = None
+ self.opening_value = None
+ self.gross_pl = None
+ self.net_pl = None
+ self.combined_position = None
+
+ # A strategy is defined in Strategies.js it is received from the client,
+ # then unpacked and converted into a python object here.
+ for name, value in args.items():
+ # Make each keyword-argument a property of the class.
+ setattr(self, name, value)
+
+ # A container to hold previous state of signals.
+ self.last_states = {}
+
+ # A list of all the trades made by this strategy.
+ self.trades = []
+
+ def get_position(self):
+ return self.combined_position
+
+ def get_pl(self):
+ self.update_pl()
+ return self.net_pl
+
+ def update_pl(self):
+ # sum the pl of all the trades.
+ position_sum = 0
+ pl_sum = 0
+ opening_value_sum = 0
+ value_sum = 0
+ for trade in self.trades:
+ pl_sum += trade.profit_loss
+ position_sum += trade.position_size
+ value_sum += trade.value
+ opening_value_sum += trade.opening_value
+ self.combined_position = position_sum
+ self.gross_pl = pl_sum
+ self.opening_value = opening_value_sum
+ self.current_value = value_sum
+
+ def to_json(self):
+ return json.dumps(self, default=lambda o: o.__dict__,
+ sort_keys=True, indent=4)
+
+ def evaluate_strategy(self, signals):
+ """
+ :param signals: Signals: A reference to an object that handles current signal states.
+ :return action: Action required based on evaluation. format{cmd:str, amount:real, margin:int}
+ """
+
+ def condition_satisfied(sig_name, value):
+ """
+ Check if a signal has a state of value.
+ :param sig_name: str: The name of a signal object to compare states.
+ :param value: The state value to compare.
+ :return bool: True: == .
+ """
+ signal = signals.get_signal_by_name(sig_name)
+ # Evaluate for a state change
+ if value == 'changed':
+ # Store the state if it hasn't been stored yet.
+ if sig_name not in self.last_states:
+ self.last_states.update({sig_name: signal.state})
+ # Store the new state and return true if the state has changed.
+ if self.last_states[sig_name] != signal.state:
+ self.last_states.update({sig_name: signal.state})
+ return True
+ else:
+ # Else return true if the values match.
+ return value == json.dumps(signal.state)
+
+ def all_conditions_met(conditions):
+ # Loops through a lists of signal names and states.
+ # Returns True if all combinations are true.
+ if len(conditions) < 1:
+ print(f"no trade-in conditions supplied: {self.name}")
+ return False
+ # Evaluate all conditions and return false if any are un-met.
+ for trigger_signal in conditions.keys():
+ trigger_value = conditions[trigger_signal]
+ # Compare this signal's state with the trigger_value
+ print(f'evaluating :({trigger_signal, trigger_value})')
+ if not condition_satisfied(trigger_signal, trigger_value):
+ print('returning false')
+ return False
+ print('all conditions met!!!')
+ return True
+
+ def trade_out_condition_met(condition_type):
+ # Retrieve the condition from either the 'stop_loss' or 'take_profit' obj.
+ condition = getattr(self, condition_type)
+ # Subtypes of conditions are 'conditional' or 'value'.
+ if condition.typ == 'conditional':
+ signal_name = condition.trig
+ signal_value = condition.val
+ return condition_satisfied(signal_name, signal_value)
+ else:
+ if condition_type == 'take_profit':
+ if self.merged_profit:
+ # If the profit condition is met send command to take profit.
+ return self.gross_profit > self.take_profit.val
+ else:
+ # Loop through each associated trade and test
+ for trade in self.trades:
+ return trade.profit_loss > self.take_profit.val
+ elif condition_type == 'value':
+ if self.merged_loss:
+ # If the loss condition is met, return a trade-out command.
+ return self.gross_loss < self.stop_loss.val
+ else:
+ # Loop through each associated trade and test
+ for trade in self.trades:
+ return trade.profit_loss < self.stop_loss.val
+ else:
+ raise ValueError('trade_out_condition_met: invalid condition_type')
+
+ trade_in_cmd = self.side
+ if self.side == 'buy':
+ trade_out_cmd = 'sell'
+ else:
+ trade_out_cmd = 'buy'
+ if self.type == 'in-out':
+ print('evaluating trade in conditions for in / out')
+ # If trade-in conditions are met.
+ if all_conditions_met(self.trd_in_conds):
+ # If the new trade wouldn't exceed max_position. Return a trade-in command.
+ proposed_position_size = int(self.combined_position) + int(self.trade_amount)
+ if proposed_position_size < int(self.max_position):
+ return 'open_position', trade_in_cmd
+
+ # If strategy is active test the take-profit or stop-loss conditions.
+ if self.active:
+ # Conditional take-profit trades-out if a signals equals a set value.
+ if trade_out_condition_met('take_profit'):
+ return 'take_profit', trade_out_cmd
+
+ # Conditional stop-loss trades-outs if a signals value equals a set value.
+ if trade_out_condition_met('stop_loss'):
+ return 'stop_loss', trade_out_cmd
+
+ # No conditions were met.
+ print('Strategies were updated and nothing to do.')
+ return 'do_nothing', 'nothing'
+
+
class Strategies:
def __init__(self, loaded_strats, trades):
- self.strat_list = loaded_strats
+ # Reference to the trades object that maintains all trading actions and data.
self.trades = trades
+ # A list of all the Strategies created.
+ self.strat_list = []
+ # Initialise all the stately objects with the data saved to file.
+ for entry in loaded_strats:
+ self.strat_list.append(Strategy(**entry))
def new_strategy(self, data):
- self.strat_list.append(data)
+ # Create an instance of the new Strategy.
+ self.strat_list.append(Strategy(**data))
def delete_strategy(self, name):
obj = self.get_strategy_by_name(name)
if obj:
self.strat_list.remove(obj)
- def get_strategies(self):
- return self.strat_list
+ def get_strategies(self, form):
+ # Return a python object of all the strategies stored in this instance.
+ if form == 'obj':
+ return self.strat_list
+ # Return a JSON object of all the strategies stored in this instance.
+ elif form == 'json':
+ strats = self.strat_list
+ json_str = []
+ for strat in strats:
+ json_str.append(strat.to_json())
+ return json_str
+ # Return a dictionary object of all the strategies stored in this instance.
+ elif form == 'dict':
+ strats = self.strat_list
+ s_list = []
+ for st in strats:
+ dic = st.__dict__
+ s_list.append(dic)
+ return s_list
def get_strategy_by_name(self, name):
for obj in self.strat_list:
- if obj['name'] == name:
+ if obj.name == name:
return obj
return False
@@ -27,33 +202,34 @@ class Strategies:
order_type = 'LIMIT'
if action == 'open_position':
# Attempt to create the trade.
- trade_id = self.trades.new_trade(strategy['symbol'], cmd, order_type, strategy['trade_amount'])
- # If the trade didn't fail.
- if trade_id is not False:
- # Set the active flag in strategy.
- strategy['active'] = True
- strategy['current_position'] += strategy['trade_amount']
- strategy['trades'].append(trade_id)
- return 'position_opened'
- else:
- print('Failed to place trade')
+ status, result = self.trades.new_trade(strategy.symbol, cmd, order_type, strategy.trade_amount)
+ # If the trade failed.
+ if status == 'Error':
+ print(status, result)
return 'failed'
+ else:
+ # Set the active flag in strategy.
+ strategy.active = True
+ strategy.current_position += strategy.trade_amount
+ strategy.trades.append(result)
+ return 'position_opened'
if (action == 'stop_loss') or (action == 'take_profit'):
if action == 'stop_loss':
order_type = 'MARKET'
# Attempt to create the trade.
- trade = self.trades.new_trade(strategy['symbol'], cmd, order_type, strategy['current_position'])
- # If the trade didn't fail.
- if trade is not False:
+ status, result = self.trades.new_trade(strategy['symbol'], cmd, order_type, strategy['current_position'])
+ # If the trade failed.
+ if status == 'Error':
+ print(status, result)
+ return 'failed'
+ else:
# Set the active flag in strategy.
strategy['active'] = False
strategy['current_position'] = 0
return 'position_closed'
- else:
- print('Failed to place trade')
- return 'failed'
- print('Strategies.execute_cmd: Invalid action received.')
+
+ print(f'Strategies.execute_cmd: Invalid action received: {action}')
return 'failed'
def update(self, signals):
@@ -62,99 +238,28 @@ class Strategies:
published strategies and evaluates conditions against the data.
This function returns a list of strategies and action commands.
"""
- # Object containing data to return to function caller.
- actions = {}
+ def process_strategy(strategy):
+ action, cmd = strategy.evaluate_strategy(signals)
+ if action != 'do_nothing':
+ # Execute the command.
+ return {'action': action, 'result': self.execute_cmd(strategy, action, cmd)}
+ return {'action': 'none'}
+
+ def get_stats(strategy):
+ position = strategy.get_position()
+ pl = strategy.get_pl()
+ stats = {'pos': position, 'pl': pl}
+ return stats
+
+ # Data object returned to function caller.
+ return_obj = {}
# Loop through all the published strategies.
for strategy in self.strat_list:
- # Process any take_profit strategy.
- if strategy['type'] == 'take_profit':
- action, cmd = self.eval_tp_stg(strategy, signals)
- if action == 'do_nothing':
- return False
- else:
- # Execute the command.
- actions[strategy['name']] = self.execute_cmd(strategy, action, cmd)
- else:
- print(f"Strategy.update: Strategy of type {strategy['type']} - not yet implemented.")
- if len(actions) != 0:
- return actions
- else:
+ actions = process_strategy(strategy)
+ stat_updates = get_stats(strategy)
+ return_obj[strategy.name] = {'actions': actions, 'stats': stat_updates}
+ if len(return_obj) == 0:
return False
-
- def eval_tp_stg(self, strategy, signals):
- """
- :param strategy: str: The strategy to evaluate.
- :param signals: Signals: A reference to an object that handles current signal states.
- :return action: Action required based on evaluation. format{cmd:str, amount:real, margin:int}
- """
-
- def condition_satisfied(sig_name, vlue):
- signal = signals.get_signal_by_name(sig_name)
- if vlue == json.dumps(signal.state):
- return True
- else:
- return False
-
- def all_conditions_met(conditions):
- if len(conditions) < 1:
- print(f"no trade-in conditions supplied: {strategy['name']}")
- return False
- # Evaluate all conditions and return false if any are un-met.
- for trigger_signal in conditions.keys():
- trigger_value = conditions[trigger_signal]
- # Compare this signal's state with the trigger_value
- if not condition_satisfied(trigger_signal, trigger_value):
- return False
- return True
-
- def trade_out_condition_met(condition_type):
- if strategy[condition_type]['typ'] == 'conditional':
- signal_name = strategy[condition_type]['trig']
- signal_value = strategy[condition_type]['val']
- if condition_satisfied(signal_name, signal_value):
- # If the condition is met trade-out.
- return True
- else:
- return False
- else:
- if strategy[condition_type]['typ'] != 'value':
- raise ValueError('trade_out_condition_met: invalid condition_type')
- if condition_type == 'take_profit':
- # If the profit condition is met send command to take profit.
- if strategy['gross_profit'] > strategy['take_profit']['val']:
- return True
- else:
- return False
- else:
- # If the loss condition is met, return a trade-out command.
- if strategy['gross_loss'] < strategy['stop_loss']['val']:
- return True
- else:
- return False
-
- trade_in_cmd = strategy['side']
- if strategy['side'] == 'buy':
- trade_out_cmd = 'sell'
else:
- trade_out_cmd = 'buy'
+ return return_obj
- # If trade-in conditions are met.
- if all_conditions_met(strategy['trd_in_conds']):
- # If the new trade wouldn't exceed max_position. Return a trade-in command.
- proposed_position_size = strategy['current_position'] + strategy['trade_amount']
- if proposed_position_size < strategy['max_position']:
- return 'enter_position', trade_in_cmd
-
- # If strategy is active test the take-profit or stop-loss conditions.
- if strategy['active']:
- # Conditional take-profit trades-out if a signals equals a set value.
- if trade_out_condition_met('take_profit'):
- return 'take_profit', trade_out_cmd
-
- # Conditional stop-loss trades-outs if a signals value equals a set value.
- if trade_out_condition_met('stop_loss'):
- return 'stop_loss', trade_out_cmd
-
- # No conditions were met.
- print('Strategies were updated and nothing to do.')
- return 'do_nothing', 'nothing'
diff --git a/app.py b/app.py
index a70abec..0ebfdb4 100644
--- a/app.py
+++ b/app.py
@@ -1,28 +1,33 @@
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 server side data and interactions.
+# Handles all updates and requests for locally stored data.
from data import BrighterData
-# Define app
-app = Flask(__name__)
-sock = Sock(app)
+# Create a Flask object named interface that serves the html.
+interface = Flask(__name__)
+# Create a socket in order to receive requests.
+sock = Sock(interface)
-# This object maintains all the application and historical data.
-# Access to server, local storage, other classes go through here.
+# 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()
-# app.config['SECRET_KEY'] = 'The quick brown fox jumps over the lazy dog'
-# app.config['CORS_HEADERS'] = 'Content-Type'
-# cors = CORS(app, resources={r"*": {"origins": "*"}})
-@app.route('/')
+# 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():
- # Passes data into an HTML template and serves it to a locally hosted server
+ # 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',
@@ -40,77 +45,111 @@ def index():
@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' in msg_obj:
- if msg_obj['message_type'] == 'candle_data':
- # Send the candle 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 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)
- else:
- print('Warning: Unhandled request!')
- print(msg_obj['data'])
-
- 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'] == 'reply':
- print(msg_obj['rep'])
- print('Reply')
-
- if msg_obj['message_type'] == 'new_signal':
- # Send the data to the BrighterData_obj
- # 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 '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:
@@ -125,38 +164,62 @@ def ws(sock):
print(f'Msg received from client: {msg}')
-@app.route('/buy', methods=['POST'])
-@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
-def buy():
- print('This buy route is currently not being used.')
- # app_data.trades.new_trade(
- # symbol=request.form['symbol'], side=SIDE_BUY,
- # type=ORDER_TYPE_MARKET, quantity=request.form['quantity'])
- return redirect('/')
+# @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('/')
-@app.route('/sell', methods=['POST'])
-@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
-def sell():
- print('This sell route is currently not being used.')
- # app_data.trades.new_trade(
- # symbol=request.form['symbol'], side=SIDE_SELL,
- # type=ORDER_TYPE_MARKET, quantity=request.form['quantity'])
- return redirect('/')
-
-
-@app.route('/settings', methods=['POST'])
+@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.chart_interval = interval_state
+ app_data.config.set_chart_interval(interval_state)
elif setting == 'trading_pair':
- trading_pair = request.form['trading_pair']
- app_data.config.trading_pair = trading_pair
+ trading_pair = request.form['symbol']
+ app_data.config.set_trading_pair(trading_pair)
elif setting == 'toggle_indicator':
- # Get a list of indicators to enable
+ # Create a list of all the enabled indicators.
enabled_indicators = []
for i in request.form:
if request.form[i] == 'indicator':
@@ -224,23 +287,27 @@ def settings():
return redirect('/')
-@app.route('/history')
+@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))
-@app.route('/saved_data')
+@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)
-@app.route('/indicator_init')
+@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)
diff --git a/candles.py b/candles.py
index 08946b0..11f3bb8 100644
--- a/candles.py
+++ b/candles.py
@@ -1,24 +1,30 @@
import datetime
import csv
+from binance.enums import HistoricalKlinesType
class Candles:
- def __init__(self, config, client):
- # Keep a reference to the exchange client.
- self.client = client
+ def __init__(self, config, exchange):
+ # Keep a reference to the exchange object.
+ self.exchange = exchange
+
# The maximum amount of data to load into memory at one time.
self.max_data_loaded = config.max_data_loaded
- # A reference to the app configuration
+
+ # A reference to the interface configuration
self.config = config
# The entire loaded candle history
self.candlesticks = []
+
# List of dictionaries of timestamped high, low, and closing values
self.latest_high_values = []
self.latest_low_values = []
self.latest_close_values = []
+
# Values of the last candle received
self.last_candle = None
+
# List of dictionaries of timestamped volume values
self.latest_vol = []
@@ -26,11 +32,15 @@ class Candles:
self.set_candle_history(symbol=config.trading_pair, interval=config.chart_interval)
def set_new_candle(self, cdata):
+ # TODO: this is not updating self.candlesticks[]. I think it is because it is
+ # todo: not in the raw format received earlier. check that the format is the same.
+ # todo: candlesticks is accessed by get_candle_history() while init the charts
self.last_candle = cdata
self.latest_close_values.append({'time': cdata['time'], 'close': cdata['close']})
self.latest_high_values.append({'time': cdata['time'], 'high': cdata['high']})
self.latest_low_values.append({'time': cdata['time'], 'low': cdata['low']})
self.latest_vol.append({'time': cdata['time'], 'value': cdata['vol']})
+ return True
def load_candle_history(self, symbol, interval):
""" Retrieve candlestick history from a file and append it with
@@ -76,7 +86,10 @@ class Candles:
# Set with the timestamp of the last candles on file
last_candle_stamp = candlesticks[-1][0]
# Request any missing candlestick data from the exchange
- recent_candlesticks = self.client.get_historical_klines(symbol, interval, start_str=int(last_candle_stamp))
+ recent_candlesticks = self.exchange.client.get_historical_klines(symbol,
+ interval,
+ start_str=int(last_candle_stamp),
+ klines_type=HistoricalKlinesType.FUTURES)
# Discard the first row of candlestick data as it will be a duplicate***DOUBLE CHECK THIS
recent_candlesticks.pop(0)
# Append the candlestick list and the file
diff --git a/data.py b/data.py
index 914d854..e2ce7fd 100644
--- a/data.py
+++ b/data.py
@@ -1,10 +1,7 @@
-from binance.client import Client
-
-import config
from Strategies import Strategies
from candles import Candles
from Configuration import Configuration
-from exchange_info import ExchangeInfo
+from exchange import Exchange
from indicators import Indicators
from Signals import Signals
from trade import Trades
@@ -14,39 +11,34 @@ import json
class BrighterData:
def __init__(self):
- # Initialise a connection to the Binance client API
- self.client = Client(config.API_KEY, config.API_SECRET)
-
- # Object that maintains signals
- self.signals = Signals()
+ # Object that interacts and maintains exchange and account data
+ self.exchange = Exchange()
# Configuration and settings for the user interface and charts
self.config = Configuration()
- # Load any saved data from file
- self.config.config_and_states('load')
-
- # Initialize signals with loaded data.
- self.signals.init_loaded_signals(self.config.signals_list)
+ # Object that maintains signals. Initialize with any signals loaded from file.
+ self.signals = Signals(self.config.signals_list)
# Object that maintains candlestick and price data.
- self.candles = Candles(self.config, self.client)
+ self.candles = Candles(self.config, self.exchange)
# Object that interacts with and maintains data from available indicators
self.indicators = Indicators(self.candles, self.config)
- # Object that maintains exchange and account data
- self.exchange_info = ExchangeInfo(self.client)
-
- # Object that maintains the strategies data
- self.trades = Trades(self.client)
+ # Object that maintains the trades data
+ self.trades = Trades(self.config.trades)
+ # The Trades object needs to connect to an exchange.
+ self.trades.connect_exchange(exchange=self.exchange)
# Object that maintains the strategies data
self.strategies = Strategies(self.config.strategies_list, self.trades)
def get_js_init_data(self):
- """Returns a JSON object of initialization data
- for the javascript in the rendered HTML"""
+ """
+ Returns a JSON object of initialization data.
+ This is passed into the frontend HTML template for the javascript to access in the rendered HTML.
+ """
js_data = {'i_types': self.indicators.indicator_types,
'indicators': self.indicators.indicator_list,
'interval': self.config.chart_interval,
@@ -55,13 +47,13 @@ class BrighterData:
def get_rendered_data(self):
"""
- Data to be rendered in the HTML
+ Data to be injected into the HTML template that renders the frontend UI.
"""
rd = {}
rd['title'] = self.config.application_title # Title of the page
- rd['my_balances'] = self.exchange_info.balances # Balances on the exchange
- rd['symbols'] = self.exchange_info.symbols # Symbols information from the exchange
- rd['intervals'] = self.exchange_info.intervals # Time candle time intervals available to stream
+ rd['my_balances'] = self.exchange.balances # Balances on the exchange
+ rd['symbols'] = self.exchange.symbols # Symbols information from the exchange
+ rd['intervals'] = self.exchange.intervals # Time candle time intervals available to stream
rd['chart_interval'] = self.config.chart_interval # The charts current interval setting
rd['indicator_types'] = self.indicators.indicator_types # All the types indicators Available
rd['indicator_list'] = self.indicators.get_indicator_list() # indicators available
@@ -70,83 +62,96 @@ class BrighterData:
return rd
def received_cdata(self, cdata):
- # If this is the first candle received,
- # then set last_candle. Then set a new candle.
+ """
+ This is called to pass in new price data when it is received.
+ :param cdata: - An object containing the most recent price data.
+ :return: - Dictionary object containing information about the updates to be passed onto the UI.
+ """
+ # If this is the first candle received last_candle will be empty.
if not self.candles.last_candle:
+ # Set last_candle.
self.candles.last_candle = cdata
- # If this candle is the same as last candle return nothing to do.
- elif cdata['time']:
- if cdata['time'] == self.candles.last_candle['time']:
- return
+ # If this is not the first candle, but it's the same as the last candle recorded.
+ elif cdata['time'] == self.candles.last_candle['time']:
+ # Return without doing anything.
+ return None
- # New candle is received update the instance data records. And the indicators.
+ # A new candle is received with a different timestamp from the last candle.
+ # Update the instance data records.
self.candles.set_new_candle(cdata)
+ # Update the indicators and receive a dictionary of indicator results.
i_updates = self.indicators.update_indicators()
- # Process the signals based on the last indicator updates.
+ # Now that all the indicators have changed. Process the signals and receive a list of signals that
+ # have changed their states.
state_changes = self.signals.process_all_signals(self.indicators)
- # Update the trades instance.
+ # Update the trades instance with the new price data.
trade_updates = self.trades.update(cdata)
- # Update the strategies instance.
+ # Update the strategies instance. Strategy execution is based on the signal states and trade values.
+ # This must be updated last.
stg_updates = self.strategies.update(self.signals)
- # Format and return an update object.
- updates = {'i_updates': i_updates}
+ # Format and return an update object to pass information to the frontend UI.
+ updates = {}
+ if i_updates:
+ updates.update({'i_updates': i_updates})
if state_changes:
print(state_changes)
updates.update({'s_updates': state_changes})
- if stg_updates:
- print(stg_updates)
- updates.update({'stg_updts': stg_updates})
-
if trade_updates:
print(trade_updates)
updates.update({'trade_updts': trade_updates})
-
+ if stg_updates:
+ print(stg_updates)
+ updates.update({'stg_updts': stg_updates})
return updates
def received_new_signal(self, data):
- # Check the data.
+ """
+ This is called when a new Signal has been defined and created in the UI.
+
+ :param data: - The attributes of the signal.
+ :return: An error if failed. On success return incoming data for chaining.
+ """
+ # Validate the incoming data.
if 'name' not in data:
return 'data.py:received_new_signal() - The new signal has no name. '
# Forward the new signal data to the signals instance. So it can create a new signal.
self.signals.new_signal(data)
- # Forward the new signal data to config. So it can save it to file.
- self.config.new_signal(data)
+ # Update config's list of signals and save it to file.
+ self.config.update_data('signals', self.signals.get_signals('dict'))
# Send the data back to where it came from.
return data
def received_new_strategy(self, data):
- # Check the data.
+ """
+ This is called when a new Strategy has been defined and created in the UI.
+
+ :param data: - The attributes of the strategy.
+ :return: An error if failed. On success return incoming data for chaining.
+ """
+ # Validate the incoming data.
if 'name' not in data:
return 'data.py:received_new_strategy() - The new strategy has no name. '
# Forward the new strategy data to the strategy's instance. So it can create a new strategy.
self.strategies.new_strategy(data)
- # Forward the new signal data to config. So it can save it to file.
- self.config.new_strategy(data)
+ # Update config's list of strategies and save to file.
+ self.config.update_data('strategies', self.strategies.get_strategies('dict'))
# Send the data back to where it came from.
return data
def delete_strategy(self, strategy_name):
- # Delete the signal from the signals instance.
+ # Delete the strategy from the strategies instance.
self.strategies.delete_strategy(strategy_name)
- # Delete the signal from the configuration file.
+ # Delete the strategy from the configuration file.
self.config.remove('strategies', strategy_name)
def get_signals(self):
- """ Return a JSON object of all the signals in the signals instance."""
- sigs = self.signals.get_signals()
- json_str = []
- for sig in sigs:
- json_str.append(json.dumps(sig.__dict__))
- return json_str
+ """Return a JSON object of all the signals in the signals instance."""
+ return self.signals.get_signals('json')
def get_strategies(self):
- """ Return a JSON object of all the signals in the signals instance."""
- strats = self.strategies.get_strategies()
- json_str = []
- for strat in strats:
- json_str.append(json.dumps(strat))
- return json_str
+ """ Return a JSON object of all the strategies in the strategies instance."""
+ return self.strategies.get_strategies('json')
def delete_signal(self, signal_name):
# Delete the signal from the signals instance.
@@ -154,3 +159,48 @@ class BrighterData:
# Delete the signal from the configuration file.
self.config.remove('signals', signal_name)
+ def received_new_trade(self, data):
+ """
+ This is called when a new trade has been defined and created in the UI.
+ Todo: Note - I handled this differently then signals and strategies. Is this better or not?
+ :param data: - The attributes of the trade.
+ :return: An error if failed. On success return incoming data to forward back to the UI.
+ """
+
+ def vld(attr):
+ # Verify and validate input data.
+ if attr in data and data[attr] != '':
+ try:
+ return float(data[attr])
+ except ValueError:
+ return data[attr]
+ else:
+ return None
+ # Forward the request to trades.
+ status, result = self.trades.new_trade(target=vld('target'), symbol=vld('symbol'), price=vld('price'),
+ side=vld('side'), order_type=vld('orderType'),
+ qty=vld('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: target={vld('target')}, symbol={vld('symbol')},"
+ f" side={vld('side')}, type={vld('orderType')}, quantity={vld('quantity')},price={vld('price')}")
+
+ # Update config's list of trades and save to file.
+ self.config.update_data('trades', self.trades.get_trades('dict'))
+ # Send the data back to where it came from.
+ return data
+
+ # todo: This is how I handled this when I was using the flask interface to catch an html form submission.
+ # # Set the default symbol to the trading pair the UI is currently focused on.
+ # if symbol is None:
+ # symbol = self.config.trading_pair
+ # self.trades.new_trade(target=target, symbol=symbol, side=side, order_type=order_type,
+ # qty=quantity, price=price, offset=None)
+ # # Update config's list of trades and save to file.
+ # self.config.update_data('trades', self.trades.get_trades('dict'))
+
+ def get_trades(self):
+ """ Return a JSON object of all the trades in the trades instance."""
+ return self.trades.get_trades('json')
diff --git a/exchange.py b/exchange.py
new file mode 100644
index 0000000..673f88f
--- /dev/null
+++ b/exchange.py
@@ -0,0 +1,93 @@
+import json
+from binance.client import Client
+from binance.exceptions import BinanceAPIException
+
+import config
+import requests
+from binance.enums import *
+
+
+class Exchange:
+ def __init__(self):
+ # Initialise a connection to the Binance client API
+ self.client = Client(config.API_KEY, config.API_SECRET)
+
+ # List of time intervals available for trading.
+ self.intervals = None
+
+ # all symbols available for trading.
+ self.symbols = None
+
+ # Any non-zero balance-info for all assets.
+ self.balances = None
+
+ # Request data from the exchange and set the above values.
+ self.get_exchange_data()
+
+ # Info on all futures symbols
+ self.futures_info = self.client.futures_exchange_info()
+
+ # Dictionary of places the exchange requires after the decimal for all symbols.
+ self.symbols_n_precision = {}
+ for item in self.futures_info['symbols']:
+ self.symbols_n_precision[item['symbol']] = item['quantityPrecision']
+
+ def get_exchange_data(self):
+ # Pull all balances from the exchange.
+ account = self.client.futures_coin_account_balance()
+ # Discard assets with zero balance.
+ self.balances = [asset for asset in account if float(asset['balance']) > 0]
+
+ # Pull all available symbols from client
+ exchange_info = self.client.futures_exchange_info()
+ self.symbols = exchange_info['symbols']
+
+ # Available intervals
+ self.intervals = (
+ KLINE_INTERVAL_1MINUTE, KLINE_INTERVAL_3MINUTE,
+ KLINE_INTERVAL_5MINUTE, KLINE_INTERVAL_15MINUTE,
+ KLINE_INTERVAL_30MINUTE, KLINE_INTERVAL_1HOUR,
+ KLINE_INTERVAL_2HOUR, KLINE_INTERVAL_4HOUR,
+ KLINE_INTERVAL_6HOUR, KLINE_INTERVAL_8HOUR,
+ KLINE_INTERVAL_12HOUR, KLINE_INTERVAL_1DAY,
+ KLINE_INTERVAL_3DAY, KLINE_INTERVAL_1WEEK,
+ KLINE_INTERVAL_1MONTH
+ )
+
+ def get_precision(self, symbol):
+ return self.symbols_n_precision[symbol]
+
+ def get_min_qty(self, symbol):
+ """
+ *TODO: what does this represent?
+ :param symbol: The symbol of the trading pair.
+ :return: The minimum quantity sold per trade.
+ """
+ info = self.client.get_symbol_info(symbol=symbol)
+ for f_index, item in enumerate(info['filters']):
+ if item['filterType'] == 'LOT_SIZE':
+ break
+ return float(info['filters'][f_index]['minQty']) # 'minQty'
+
+ def get_min_notional_qty(self, symbol):
+ """
+ * Not Working: Can't buy this amount??? TODO: what does this represent?
+ :param symbol: The symbol of the trading pair.
+ :return: The minimum quantity sold per trade.
+ """
+ info = self.client.get_symbol_info(symbol=symbol)
+ for f_index, item in enumerate(info['filters']):
+ if item['filterType'] == 'MIN_NOTIONAL':
+ break
+ return float(info['filters'][f_index]['minNotional']) # 'minNotional'
+
+ @staticmethod
+ def get_price(symbol):
+ """
+ :param symbol: The symbol of the trading pair.
+ :return: The current ticker price.
+ """
+ # Todo: Make sure im getting the correct market price for futures.
+ request = requests.get(f'https://api.binance.com/api/v3/ticker/price?symbol={symbol}')
+ json_obj = json.loads(request.text)
+ return float(json_obj['price'])
diff --git a/exchange_info.py b/exchange_info.py
deleted file mode 100644
index 57a9aa0..0000000
--- a/exchange_info.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from binance.enums import *
-
-
-class ExchangeInfo:
- def __init__(self, client):
- self.client = client
-
- self.intervals = None
- self.symbols = None
- self.balances = None
- # Set the above values from information retrieved from exchange.
- self.set_exchange_data()
-
- def set_exchange_data(self):
- # Pull all balances from client while discarding assets with zero balance
- account = self.client.futures_coin_account_balance()
- self.balances = [asset for asset in account if float(asset['balance']) > 0]
-
- # Pull all available symbols from client
- exchange_info = self.client.get_exchange_info()
- self.symbols = exchange_info['symbols']
-
- # Available intervals
- self.intervals = (
- KLINE_INTERVAL_1MINUTE, KLINE_INTERVAL_3MINUTE,
- KLINE_INTERVAL_5MINUTE, KLINE_INTERVAL_15MINUTE,
- KLINE_INTERVAL_30MINUTE, KLINE_INTERVAL_1HOUR,
- KLINE_INTERVAL_2HOUR, KLINE_INTERVAL_4HOUR,
- KLINE_INTERVAL_6HOUR, KLINE_INTERVAL_8HOUR,
- KLINE_INTERVAL_12HOUR, KLINE_INTERVAL_1DAY,
- KLINE_INTERVAL_3DAY, KLINE_INTERVAL_1WEEK,
- KLINE_INTERVAL_1MONTH
- )
diff --git a/indicators.py b/indicators.py
index 53c6fa2..f20818e 100644
--- a/indicators.py
+++ b/indicators.py
@@ -398,25 +398,28 @@ class Indicators:
enabled_indicators.append(indctr)
return enabled_indicators
- def get_indicator_data(self, symbol=None, interval=None, num_results=800):
+ def get_indicator_data(self, symbol=None, interval=None, start_ts=None, num_results=800):
# Loop through all the indicators. If enabled, run the appropriate
# update function. Return all the results as a dictionary object.
if symbol is not None:
- print(symbol)
- print('get_indicator_data() no symbol implementation')
+ if self.candles.config.trading_pair != symbol:
+ print(f'get_indicator_data: request candle data for {symbol}')
if interval is not None:
- print(interval)
- print('get_indicator_data() no interval implementation')
+ if self.candles.config.chart_interval != interval:
+ print(f'get_indicator_data: request candle data for {interval}')
+ if start_ts is not None:
+ print(f'get_indicator_data: request candle data from: {start_ts}')
# Get a list of indicator objects and a list of enabled indicators names.
i_list = self.indicators
enabled_i = self.get_enabled_indicators()
+ candles = self.candles
result = {}
# Loop through all indicator objects in i_list
for each_i in i_list:
# If the indicator's not enabled skip to next each_i
if each_i not in enabled_i:
continue
- result[each_i] = i_list[each_i].calculate(self.candles, num_results)
+ result[each_i] = i_list[each_i].calculate(candles, num_results)
return result
def delete_indicator(self, indicator):
diff --git a/requirements.txt b/requirements.txt
index 03cec72..67e3b2a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,4 +2,5 @@ numpy~=1.22.3
flask~=2.1.2
config~=0.5.1
PyYAML~=6.0
-binance
\ No newline at end of file
+binance~=0.3
+requests~=2.27.1
\ No newline at end of file
diff --git a/static/Strategies.js b/static/Strategies.js
index f27015a..c4e2052 100644
--- a/static/Strategies.js
+++ b/static/Strategies.js
@@ -1,35 +1,4 @@
-//TODO this func is unused left over for reference.
-//conditions_satisfied(signals){
-// let result = true;
-// for(let cond of this.conditions){
-// if (cond.trig_val == 'true'){
-// if (signals[cond.on] == true){
-// result = result && true;
-// }else{
-// result = false;
-// }
-// }
-// else if (cond.trig_val == 'false'){
-// if (signals[cond.on] == false){
-// result = result && true;
-// }else{
-// result = false;
-// }
-// }
-// else if (cond.trig_val == 'changed'){
-// // If no last result exists for this trigger, create one.
-// if ( !this.last_results[cond.on] ){
-// this.last_results[cond.on] = signals[cond.on];
-// }
-// if (signals[cond.on] != this.last_results[cond.on]){
-// result = result && true;
-// }else{
-// result = false;
-// }
-// }
-// }
-// return result;
-//}
+
class Strategies {
constructor(target_id) {
// The list of strategies.
@@ -47,10 +16,10 @@ class Strategies {
submit(){
/*
- Collect the data from the form fields and
- create an object representing the strategy.
- - Record the strategy in the an instance list.
+ create a json object representing a strategy.
+ - Append the strategy to a local list of strategies.
- Update the display output.
- - Send the strategy object to the server.
+ - Send the strategy info to the server.
*/
let strat = {};
strat.name = document.getElementById('stg_name').value;
@@ -58,27 +27,41 @@ class Strategies {
alert('Please provide a name.');
return;
}
+ // The type of strategy will determine underlying feature of its execution.
strat.type = document.getElementById('strat_type').value;
+ // Whether the strategy is bullish or bearish.
strat.side = document.getElementById('trade_in_side').value;
- strat.margin = document.getElementById('margin_select').value;
+ // Position size for each trade the strategy executes.
strat.trade_amount = document.getElementById('trade_amount').value;
+ // The maximum combined position allowed.
strat.max_position = document.getElementById('strgy_total').value;
+ // The fee the exchange charges per trade.
strat.trading_fee = document.getElementById('fee').value;
+ // The maximum allowable loss the strategy can endure in combined trades.
+ // Before trading out and terminating execution.
strat.max_loss = document.getElementById('max_loss').value;
+ // The trading pair being exchanged.
strat.symbol = window.UI.data.trading_pair;
- strat.net_profit = 0;
- strat.gross_profit = 0;
- strat.net_loss = 0;
- strat.gross_loss = 0;
- strat.current_position = 0;
+ // The combined profit or loss including trading fees.
+ strat.net_pl = 0;
+ // The combined profit or loss.
+ strat.gross_pl = 0;
+ // The quantity of the traded assets being held.
+ strat.combined_position = 0;
+ // Opening value of the asset.
+ strat.opening_value = 0;
+ // The current value of the asset.
strat.current_value = 0;
+ // Whether or not the strategy has begun trading.
strat.active = false;
+ // The conditions that must be met before trading in.
strat.trd_in_conds = {};
let conds = Array.from(document.querySelectorAll('#trade_in_cond>li'));
for (let cond of conds){
let json_obj = JSON.parse(cond.innerHTML);
strat.trd_in_conds[json_obj.Trigger] = json_obj.Value;
}
+ // The conditions that must be met before taking profit.
let take_profit = {};
take_profit.typ = document.getElementById('prof_typ').value;
if (take_profit.typ == 'conditional'){
@@ -88,6 +71,8 @@ class Strategies {
take_profit.val = document.getElementById('prof_val').value;
}
strat.take_profit = take_profit;
+
+ // The conditions that must be met before taking a loss.
let stop_loss = {};
stop_loss.typ = document.getElementById('loss_typ').value;
if ( stop_loss.typ == 'conditional' ){
@@ -97,12 +82,17 @@ class Strategies {
stop_loss.val = document.getElementById('loss_val').value;
}
strat.stop_loss = stop_loss;
+
// Add the strategy to the instance list.
this.strategies.push(strat);
+
// Add the strategy to display.
this.update_html();
+
// Send the new strategy to the server.
window.UI.data.comms.send_to_app( "new_strategy", strat);
+
+ // Close the html form.
this.close_form();
}
update_received(stg_updts){
@@ -124,7 +114,7 @@ class Strategies {
}
open_stg_form(){
this.open_form();
- this.fill_field('strat_opt', 'take_profit');
+ this.fill_field('strat_opt', 'in-out');
}
clear_innerHTML(el){
el.innerHTML="";
@@ -190,7 +180,7 @@ class Strategies {
if (field == 'strat_opt'){
let options = document.getElementById('strat_opt');
- if (value == 'take_profit'){
+ if (value == 'in-out'){
// Clear previous content.
this.clear_innerHTML(options);
diff --git a/static/backtesting.js b/static/backtesting.js
new file mode 100644
index 0000000..f812c3c
--- /dev/null
+++ b/static/backtesting.js
@@ -0,0 +1,6 @@
+
+class Backtesting {
+ constructor() {
+ this.height = height;
+ }
+}
\ No newline at end of file
diff --git a/static/brighterStyles.css b/static/brighterStyles.css
index ff6b356..068197d 100644
--- a/static/brighterStyles.css
+++ b/static/brighterStyles.css
@@ -149,10 +149,10 @@ input[type="checkbox"] {
#chart_controls{
border-style:none;
- width: 200px;
+ width: 775px;
padding: 15px;
display: grid;
- grid-template-columns:700px 1fr 1fr;
+ grid-template-columns:350px 2fr 1fr 1fr;
}
#indicators{
@@ -186,7 +186,6 @@ input[type="checkbox"] {
}
/***************************************************/
-
/****************Right Panel***********************/
.collapsible {
background-color: #3E3AF2;
@@ -272,9 +271,24 @@ position: absolute;
.new_btn{
margin:5;
}
+/*******************Trade Panel***************************/
+#price{
+ height:20px;
+ display:none;
+}
+#current_price{
+ height:20px;
+ display:inline-block;
+}
+
+#price-label{
+ height:20px;
+}
+#quantity{
+ height:20px;
+}
+
/***************************************************/
-
-
/************Edit/Add Indicators Panel**************/
#edit_indcr_panel{
width: 1000px;
diff --git a/static/communication.js b/static/communication.js
index 84e2267..c394d0f 100644
--- a/static/communication.js
+++ b/static/communication.js
@@ -1,27 +1,54 @@
class Comms {
- constructor(ocu, occ, oiu) {
- // Register callbacks
- this.on_candle_update = ocu;
- this.on_candle_close = occ;
- this.on_indctr_update = oiu;
+ constructor() {
+ // List of callbacks callback function that will receive various updates.
+ this.on_candle_update = [];
+ this.on_candle_close = [];
+ this.on_indctr_update = [];
+ // Status of connection.
this.connection_open = false;
}
-
+ register_callback(target, callback_func){
+ // Register any outside functions that need to receive updates.
+ if (target=='candle_update'){
+ this.on_candle_update.push(callback_func)
+ }
+ else if (target=='candle_close'){
+ this.on_candle_close.push(callback_func)
+ }
+ else if (target=='indicator_update'){
+ this.on_indctr_update.push(callback_func)
+ }
+ else{console.log('Comms: Invalid target for callback');}
+ }
candle_update(new_candle){
- // Call the callback provided.
- this.on_candle_update(new_candle);
+ // If no callback is registered do nothing.
+ if (this.on_candle_update == null) {return};
+ // Call each registered callback passing the candle updates.
+ for (i = 0; i < this.on_candle_update.length; i++) {
+ this.on_candle_update[i](new_candle);
+ }
}
candle_close(new_candle){
+
// Forward a copy of the new candle to the server.
this.app_con.send( JSON.stringify({ message_type: "candle_data", data :new_candle }));
- // Call the callback provided.
- this.on_candle_close(new_candle);
+
+ // If no callback is registered do return.
+ if (this.on_candle_close == null) {return};
+ // Call each registered callback passing the candle updates.
+ for (i = 0; i < this.on_candle_close.length; i++) {
+ this.on_candle_close[i](new_candle);
+ }
}
indicator_update(data){
- // Call the callback provided.
- this.on_indctr_update(data);
+ // If no callback is registered do return.
+ if (this.on_indctr_update == null) {return};
+ // Call each registered callback passing the candle updates.
+ for (i = 0; i < this.on_indctr_update.length; i++) {
+ this.on_indctr_update[i](data);
+ }
}
getPriceHistory(){
@@ -113,8 +140,9 @@ class Comms {
})
}
- set_exchange_con(interval){
- let ws = "wss://stream.binance.com:9443/ws/btcusdt@kline_" + interval;
+ set_exchange_con(interval, tradingPair){
+ tradingPair = tradingPair.toLowerCase();
+ let ws = "wss://stream.binance.com:9443/ws/" + tradingPair + "@kline_" + interval;
this.exchange_con = new WebSocket(ws);
// Set the on-message call-back for the socket
diff --git a/static/controls.js b/static/controls.js
index 071577f..5fa5390 100644
--- a/static/controls.js
+++ b/static/controls.js
@@ -26,5 +26,20 @@ class Controls {
el.style.top = y + "px";
el.style.display = "block";
}
+ init_TP_selector(){
+ var demoInput = document.getElementById('symbol');
+ demoInput.value = window.UI.data.trading_pair; // set default value instead of html attribute
+ demoInput.onfocus = function() { demoInput.value =''; }; // on focus - clear input
+ demoInput.onblur = function() { demoInput.value = window.UI.data.trading_pair; }; // on leave restore it.
+ demoInput.onchange = function() {
+ var options = document.getElementById('symbols').getElementsByTagName('option');
+ this.value = this.value.toUpperCase();
+ for (let i=0; i < options.length; i += 1) {
+ if (options[i].value == this.value){
+ document.getElementById('tp_selector').submit();
+ }
+ }
+ };
+ }
}
diff --git a/static/data.js b/static/data.js
index 0d44c35..fe9fea8 100644
--- a/static/data.js
+++ b/static/data.js
@@ -14,18 +14,24 @@ class Data {
// All the indicators available.
this.indicators = bt_data.indicators;
- /* Comms handles communication with the servers. Pass it
- a list of callbacks to handle various incoming messages.*/
- this.comms = new Comms(this.candle_update, this.candle_close, this.indicator_update);
+ /* Comms handles communication with the servers. Register
+ callbacks to handle various incoming messages.*/
+ this.comms = new Comms();
+ this.comms.register_callback('candle_update', this.candle_update)
+ this.comms.register_callback('candle_close', this.candle_close)
+ this.comms.register_callback('indicator_update', this.indicator_update)
// Open the connection to our local server.
this.comms.set_app_con();
/* Open connection for streaming candle data wth the exchange.
Pass it the time period of candles to stream. */
- this.comms.set_exchange_con(this.interval);
+ this.comms.set_exchange_con(this.interval, this.trading_pair);
//Request historical price data from the server.
this.price_history = this.comms.getPriceHistory();
+ //last price from price history.
+ this.price_history.then((value) => { this.last_price = value[value.length-1].close; });
+
// Request from the server initialization data for the indicators.
this.indicator_data = this.comms.getIndicatorData();
@@ -37,7 +43,7 @@ class Data {
// This is called everytime a candle update comes from the local server.
window.UI.charts.update_main_chart(new_candle);
//console.log('Candle update:');
- //console.log(new_candle);
+ this.last_price = new_candle.close;
}
set_i_updates(call_back){
this.i_updates = call_back;
diff --git a/static/general.js b/static/general.js
index 0cc64da..995f0a9 100644
--- a/static/general.js
+++ b/static/general.js
@@ -1,15 +1,3 @@
-//
-//class Backtesting {
-// constructor() {
-// this.height = height;
-// }
-//}
-//
-//class Trade {
-// constructor() {
-// this.height = height;
-// }
-//}
//class Exchange_Info {
// constructor() {
// this.height = height;
@@ -21,14 +9,6 @@
// this.height = height;
// }
//}
-//
-//class Statistics {
-// constructor() {
-// this.height = height;
-// }
-//}
-//
-
class User_Interface{
/* This contains all the code for our User interface.
@@ -52,11 +32,17 @@ class User_Interface{
this.alerts = new Alerts("alert_list");
/* The object that handles alerts. Pass in the html
- element that will hold the list of alerts*/
+ element that will hold the strategies interface.*/
this.strats = new Strategies("strats_display");
- /* These classes interact with HTML elements that need to be parsed first.
- TODO: Can any of these objects be created then run init functions after loading?*/
+ /* The object that handles trades. Pass in the html
+ element that will hold the trade interface.*/
+ this.trade = new Trade();
+
+ /* This javascript class is loaded at the top of the main html document.
+ These classes interact with HTML elements that get parsed later in the main html document.
+ They are wrapped inside a function that executes after the entire document is loaded.
+ TODO: Decouple these object from these elements. Initialize after first call?.*/
window.addEventListener('load', function () {
/* Charts object is responsible for maintaining the
data visualisation area in the UI. */
@@ -87,6 +73,9 @@ class User_Interface{
window.UI.alerts.set_target();
// initialize the strategies instance.
window.UI.strats.initialize();
+ // initialize the controls instance.
+ window.UI.controls.init_TP_selector();
+ window.UI.trade.initialize();
});
}
diff --git a/static/signals.js b/static/signals.js
index d745fef..617d3c6 100644
--- a/static/signals.js
+++ b/static/signals.js
@@ -3,30 +3,36 @@ class Signals {
this.indicators = indicators;
this.signals=[];
}
- // Call to display Create new signal dialog.
+ // Call to display the 'Create new signal' dialog.
open_signal_Form() { document.getElementById("new_sig_form").style.display = "grid"; }
- // Call to hide Create new signal dialog.
+ // Call to hide the 'Create new signal' dialog.
close_signal_Form() { document.getElementById("new_sig_form").style.display = "none"; }
request_signals(){
+ // Requests a list of all the signals from the server.
window.UI.data.comms.send_to_app('request', 'signals');
}
delete_signal(signal_name){
+ // Requests that the server remove a specific signal.
window.UI.data.comms.send_to_app('delete_signal', signal_name);
- // Get the child element node
+ // Get the signal element from the UI
let child = document.getElementById(signal_name + '_item');
- // Remove the child element from the document
+ // Ask the parent of the signal element to remove its child(signal) from the document.
child.parentNode.removeChild(child);
}
i_update(updates){
- // Update the values listed in the signals section.
+ // Set local records of incoming signal updates and update the html that displays that info.
for (let signal in this.signals){
+ // Get the new value of the property from the 1st source in this signal from the update.
let value1 = updates[this.signals[signal].source1].data[0][this.signals[signal].prop1];
+ // Update the local record of value1
this.signals[signal].value1 = value1.toFixed(2);
if (this.signals[signal].source2 != 'value'){
+ // Do the same for the second source if the source isn't a set value.
let value2 = updates[this.signals[signal].source2].data[0][this.signals[signal].prop2];
this.signals[signal].value2 = value2.toFixed(2);
}
+ // Update the html element that displays this information.
document.getElementById(this.signals[signal].name + '_value1').innerHTML = this.signals[signal].value1;
document.getElementById(this.signals[signal].name + '_value2').innerHTML = this.signals[signal].value2;
}
@@ -254,5 +260,6 @@ class Signals {
than passing functions, references and callbacks around. */
window.UI.data.comms.send_to_app( "new_signal", data);
+ this.close_signal_Form();
}
}
\ No newline at end of file
diff --git a/static/statistics.js b/static/statistics.js
new file mode 100644
index 0000000..6002df4
--- /dev/null
+++ b/static/statistics.js
@@ -0,0 +1,5 @@
+class Statistics {
+ constructor() {
+ this.height = height;
+ }
+}
\ No newline at end of file
diff --git a/static/trade.js b/static/trade.js
new file mode 100644
index 0000000..be0e79d
--- /dev/null
+++ b/static/trade.js
@@ -0,0 +1,58 @@
+class Trade {
+ constructor() {
+ this.name = 'trade';
+ }
+ initialize(){
+ const price_input = document.getElementById('price');
+ const cp = document.getElementById('current_price');
+ window.UI.data.price_history.then((value) => { price_input.value = value[value.length-1].close;cp.value = price_input.value });
+ const qty_input = document.getElementById('quantity');
+ const tv_input = document.getElementById('trade_value');
+ tv_input.value = 0;
+ const orderType = document.getElementById('orderType');
+ orderType.addEventListener('change', function(){
+ if(this.value == 'MARKET'){
+ cp.style.display = "inline-block";
+ price_input.style.display = "none";
+ tv_input.value = qty_input.value * cp.value;
+ } else if(this.value == 'LIMIT') {
+ cp.style.display = "none";
+ price_input.style.display = "inline-block";
+ tv_input.value = qty_input.value * price_input.value;
+ }
+
+ });
+ price_input.addEventListener('change', function(){
+ if (orderType.value!='MARKET'){
+ tv_input.value = qty_input.value * price_input.value;
+ }else{
+ tv_input.value = qty_input.value * cp.value;
+ }
+ });
+ qty_input.addEventListener('change', function(){
+ if (orderType.value!='MARKET'){
+ tv_input.value = qty_input.value * price_input.value;
+ }else{
+ tv_input.value = qty_input.value * cp.value;
+ }
+ });
+
+ }
+ // Call to display the 'Create new trade' dialog.
+ open_tradeForm() { document.getElementById("new_trade_form").style.display = "grid"; }
+ // Call to hide the 'Create new signal' dialog.
+ close_tradeForm() { document.getElementById("new_trade_form").style.display = "none"; }
+
+ submitNewTrade(){
+ // Collect all the input fields.
+ var target = document.getElementById('trade_target').value; // The target to trade ['exchange'|'backtester'].
+ var symbol = window.UI.data.trading_pair; // The symbol to trade at.
+ var price = document.getElementById('price').value; // The price to trade at.
+ var side = document.getElementById('side').value; // The side to trade at.
+ var orderType = document.getElementById('orderType').value; // The orderType for the trade.
+ var quantity = document.getElementById('quantity').value; // The base quantity to trade.
+ var data = {target, symbol, price, side, orderType, quantity};
+ window.UI.data.comms.send_to_app( "new_trade", data);
+ this.close_tradeForm();
+ }
+}
diff --git a/templates/index.html b/templates/index.html
index c219be2..30dc1d3 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -19,12 +19,61 @@
+
+
+