Classes implemented in python and javascript. UML class diagram. Rough sequence uml. TODO: local file getting dirty from refresh. Signals implemented. Strategies implemented. Trades implemented but still needs some testing.

This commit is contained in:
Rob 2023-02-17 22:47:13 -04:00
parent 934a66012d
commit 79d2a9c597
26 changed files with 2042 additions and 621 deletions

View File

@ -11,4 +11,7 @@
<option name="removeUnused" value="true" /> <option name="removeUnused" value="true" />
<option name="modifyBaseFiles" value="true" /> <option name="modifyBaseFiles" value="true" />
</component> </component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="py.test" />
</component>
</module> </module>

View File

@ -31,21 +31,30 @@ class Configuration:
# list of strategies in config. # list of strategies in config.
self.strategies_list = [] self.strategies_list = []
# list of trades.
self.trades = []
# The data that will be saved and loaded from file . # The data that will be saved and loaded from file .
self.saved_data = None self.saved_data = None
def new_strategy(self, data): # Load any saved data from file
# The strategies_list is modified by reference in strategies the loaded in config_and_states('save'). self.config_and_states('load')
# Save it to file.
self.config_and_states('save')
def new_signal(self, data): def update_data(self, data_type, data):
# Create a new signal. # Replace current list of data sets with an updated list.
self.saved_data['signals'].append(data) 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. # Save it to file.
self.config_and_states('save') self.config_and_states('save')
def remove(self, what, name): def remove(self, what, name):
# Removes by name an item from a list in saved data.
print(f'removing {what}:{name}') print(f'removing {what}:{name}')
for obj in self.saved_data[what]: for obj in self.saved_data[what]:
if obj['name'] == name: if obj['name'] == name:
@ -54,6 +63,12 @@ class Configuration:
# Save it to file. # Save it to file.
self.config_and_states('save') 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): def config_and_states(self, cmd):
"""Loads or saves configurable data to the file set in self.config_FN""" """Loads or saves configurable data to the file set in self.config_FN"""
@ -62,16 +77,29 @@ class Configuration:
'indicator_list': self.indicator_list, 'indicator_list': self.indicator_list,
'config': {'chart_interval': self.chart_interval, 'trading_pair': self.trading_pair}, 'config': {'chart_interval': self.chart_interval, 'trading_pair': self.trading_pair},
'signals': self.signals_list, 'signals': self.signals_list,
'strategies': self.strategies_list 'strategies': self.strategies_list,
'trades': self.trades
} }
def set_loaded_values(): def set_loaded_values():
# Sets the values in the saved_data object. # Sets the values in the saved_data object.
self.indicator_list = self.saved_data['indicator_list'] if 'indicator_list' in self.saved_data:
self.chart_interval = self.saved_data['config']['chart_interval'] self.indicator_list = self.saved_data['indicator_list']
self.trading_pair = self.saved_data['config']['trading_pair']
self.signals_list = self.saved_data['signals'] if 'chart_interval' in self.saved_data['config']:
self.strategies_list = self.saved_data['strategies'] 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): def load_configuration(filepath):
"""load file data""" """load file data"""
@ -88,13 +116,15 @@ class Configuration:
# If load_configuration() finds a file it overwrites # If load_configuration() finds a file it overwrites
# the saved_data object otherwise it creates a new file # the saved_data object otherwise it creates a new file
# with the defaults contained in saved_data> # with the defaults contained in saved_data>
# If file exist load the values.
try: try:
# If file exist load the values.
self.saved_data = load_configuration(self.config_FN) self.saved_data = load_configuration(self.config_FN)
set_loaded_values() set_loaded_values()
# If file doesn't exist create a file and save the default values.
except IOError: except IOError:
# If file doesn't exist create a file and save the default values.
save_configuration(self.config_FN, self.saved_data) save_configuration(self.config_FN, self.saved_data)
elif cmd == 'save': elif cmd == 'save':
try: try:
# Write saved_data to the file. # Write saved_data to the file.

View File

@ -1,3 +1,4 @@
import json
from dataclasses import dataclass from dataclasses import dataclass
@ -51,19 +52,14 @@ class Signal:
class Signals: class Signals:
def __init__(self): def __init__(self, loaded_signals=None):
self.signals = []
# self.set_signals_defaults()
def set_signals_defaults(self): # list of Signal objects.
"""These defaults are loaded if the config file is not found.""" self.signals = []
sigs = self.get_signals_defaults()
for sig in sigs: # Initialize signals with loaded data.
self.signals.append(Signal(name=sig['name'], source1=sig['source1'], if loaded_signals is not None:
prop1=sig['prop1'], operator=sig['operator'], self.create_signal_from_dic(loaded_signals)
source2=sig['source2'], prop2=sig['prop2'],
state=sig['state']))
return
@staticmethod @staticmethod
def get_signals_defaults(): def get_signals_defaults():
@ -85,15 +81,41 @@ class Signals:
"value2": None, "range": None} "value2": None, "range": None}
return [s1, s2, s3] return [s1, s2, s3]
def init_loaded_signals(self, signals_list): def create_signal_from_dic(self, signals_list=None):
for sig in signals_list: """
self.signals.append(Signal(name=sig['name'], source1=sig['source1'], :param signals_list: list of dict
prop1=sig['prop1'], operator=sig['operator'], :return True: on success.
source2=sig['source2'], prop2=sig['prop2'], Create and store signal objects from list of dictionaries.
state=sig['state'])) """
def get_signals(self): # If no signals were provided used a default list.
return self.signals 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): def get_signal_by_name(self, name):
for signal in self.signals: for signal in self.signals:

View File

@ -1,25 +1,200 @@
import json 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:sig_name:state> == <value>.
"""
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: class Strategies:
def __init__(self, loaded_strats, trades): 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 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): 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): def delete_strategy(self, name):
obj = self.get_strategy_by_name(name) obj = self.get_strategy_by_name(name)
if obj: if obj:
self.strat_list.remove(obj) self.strat_list.remove(obj)
def get_strategies(self): def get_strategies(self, form):
return self.strat_list # 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): def get_strategy_by_name(self, name):
for obj in self.strat_list: for obj in self.strat_list:
if obj['name'] == name: if obj.name == name:
return obj return obj
return False return False
@ -27,33 +202,34 @@ class Strategies:
order_type = 'LIMIT' order_type = 'LIMIT'
if action == 'open_position': if action == 'open_position':
# Attempt to create the trade. # Attempt to create the trade.
trade_id = self.trades.new_trade(strategy['symbol'], cmd, order_type, strategy['trade_amount']) status, result = self.trades.new_trade(strategy.symbol, cmd, order_type, strategy.trade_amount)
# If the trade didn't fail. # If the trade failed.
if trade_id is not False: if status == 'Error':
# Set the active flag in strategy. print(status, result)
strategy['active'] = True
strategy['current_position'] += strategy['trade_amount']
strategy['trades'].append(trade_id)
return 'position_opened'
else:
print('Failed to place trade')
return 'failed' 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') or (action == 'take_profit'):
if action == 'stop_loss': if action == 'stop_loss':
order_type = 'MARKET' order_type = 'MARKET'
# Attempt to create the trade. # Attempt to create the trade.
trade = self.trades.new_trade(strategy['symbol'], cmd, order_type, strategy['current_position']) status, result = self.trades.new_trade(strategy['symbol'], cmd, order_type, strategy['current_position'])
# If the trade didn't fail. # If the trade failed.
if trade is not False: if status == 'Error':
print(status, result)
return 'failed'
else:
# Set the active flag in strategy. # Set the active flag in strategy.
strategy['active'] = False strategy['active'] = False
strategy['current_position'] = 0 strategy['current_position'] = 0
return 'position_closed' return 'position_closed'
else:
print('Failed to place trade') print(f'Strategies.execute_cmd: Invalid action received: {action}')
return 'failed'
print('Strategies.execute_cmd: Invalid action received.')
return 'failed' return 'failed'
def update(self, signals): def update(self, signals):
@ -62,99 +238,28 @@ class Strategies:
published strategies and evaluates conditions against the data. published strategies and evaluates conditions against the data.
This function returns a list of strategies and action commands. This function returns a list of strategies and action commands.
""" """
# Object containing data to return to function caller. def process_strategy(strategy):
actions = {} 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. # Loop through all the published strategies.
for strategy in self.strat_list: for strategy in self.strat_list:
# Process any take_profit strategy. actions = process_strategy(strategy)
if strategy['type'] == 'take_profit': stat_updates = get_stats(strategy)
action, cmd = self.eval_tp_stg(strategy, signals) return_obj[strategy.name] = {'actions': actions, 'stats': stat_updates}
if action == 'do_nothing': if len(return_obj) == 0:
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:
return False 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: 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'

279
app.py
View File

@ -1,28 +1,33 @@
import json import json
# Flask is a lightweight html server used to render the UI.
from flask import Flask, render_template, request, redirect, jsonify from flask import Flask, render_template, request, redirect, jsonify
from flask_cors import cross_origin from flask_cors import cross_origin
from binance.enums import * from binance.enums import *
from flask_sock import Sock 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 from data import BrighterData
# Define app # Create a Flask object named interface that serves the html.
app = Flask(__name__) interface = Flask(__name__)
sock = Sock(app) # Create a socket in order to receive requests.
sock = Sock(interface)
# This object maintains all the application and historical data. # Create a BrighterData object. This the main application that maintains access to the server, local storage,
# Access to server, local storage, other classes go through here. # and manages objects that process trade data.
app_data = BrighterData() 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(): 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() rendered_data = app_data.get_rendered_data()
js_data = app_data.get_js_init_data() js_data = app_data.get_js_init_data()
return render_template('index.html', return render_template('index.html',
@ -40,77 +45,111 @@ def index():
@sock.route('/ws') @sock.route('/ws')
def ws(sock): 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): def json_msg_received(msg_obj):
if 'message_type' in msg_obj: if 'message_type' not 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)
return 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 # The rendered page connects to the exchange and relays the candle data back here
# this socket also handles data and processing requests # this socket also handles data and processing requests
while True: while True:
@ -125,38 +164,62 @@ def ws(sock):
print(f'Msg received from client: {msg}') print(f'Msg received from client: {msg}')
@app.route('/buy', methods=['POST']) # @interface.route('/trade', methods=['POST'])
@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization']) # @cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
def buy(): # def trade():
print('This buy route is currently not being used.') # # todo: This interface method is a straight forward approach to communicating from the frontend to backend
# app_data.trades.new_trade( # # it only require an html form on the UI side. It's difficult to return a response without refreshing the UI.
# symbol=request.form['symbol'], side=SIDE_BUY, # # I may be missing a solution that doesn't require rendering a hidden frame in the UI and complicating the code
# type=ORDER_TYPE_MARKET, quantity=request.form['quantity']) # # further. I am abandoning this approach for socket method above. Although I am uncomfortable with the loop
return redirect('/') # # 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']) @interface.route('/settings', 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'])
@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization']) @cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
def settings(): 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'] setting = request.form['setting']
if setting == 'interval': if setting == 'interval':
interval_state = request.form['timeframe'] interval_state = request.form['timeframe']
app_data.config.chart_interval = interval_state app_data.config.set_chart_interval(interval_state)
elif setting == 'trading_pair': elif setting == 'trading_pair':
trading_pair = request.form['trading_pair'] trading_pair = request.form['symbol']
app_data.config.trading_pair = trading_pair app_data.config.set_trading_pair(trading_pair)
elif setting == 'toggle_indicator': elif setting == 'toggle_indicator':
# Get a list of indicators to enable # Create a list of all the enabled indicators.
enabled_indicators = [] enabled_indicators = []
for i in request.form: for i in request.form:
if request.form[i] == 'indicator': if request.form[i] == 'indicator':
@ -224,23 +287,27 @@ def settings():
return redirect('/') return redirect('/')
@app.route('/history') @interface.route('/history')
@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization']) @cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
def history(): 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 symbol = app_data.config.trading_pair
interval = app_data.config.chart_interval interval = app_data.config.chart_interval
return jsonify(app_data.candles.get_candle_history(symbol, interval, 1000)) 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']) @cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
def saved_data(): def saved_data():
# Returns the saved settings for the indicators
return jsonify(app_data.indicators.indicator_list) return jsonify(app_data.indicators.indicator_list)
@app.route('/indicator_init') @interface.route('/indicator_init')
@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization']) @cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
def indicator_init(): def indicator_init():
# Initializes the indicators and returns the data for a given symbol and interval.
symbol = app_data.config.trading_pair symbol = app_data.config.trading_pair
interval = app_data.config.chart_interval interval = app_data.config.chart_interval
d = app_data.indicators.get_indicator_data(symbol, interval, 800) d = app_data.indicators.get_indicator_data(symbol, interval, 800)

View File

@ -1,24 +1,30 @@
import datetime import datetime
import csv import csv
from binance.enums import HistoricalKlinesType
class Candles: class Candles:
def __init__(self, config, client): def __init__(self, config, exchange):
# Keep a reference to the exchange client. # Keep a reference to the exchange object.
self.client = client self.exchange = exchange
# The maximum amount of data to load into memory at one time. # The maximum amount of data to load into memory at one time.
self.max_data_loaded = config.max_data_loaded self.max_data_loaded = config.max_data_loaded
# A reference to the app configuration
# A reference to the interface configuration
self.config = config self.config = config
# The entire loaded candle history # The entire loaded candle history
self.candlesticks = [] self.candlesticks = []
# List of dictionaries of timestamped high, low, and closing values # List of dictionaries of timestamped high, low, and closing values
self.latest_high_values = [] self.latest_high_values = []
self.latest_low_values = [] self.latest_low_values = []
self.latest_close_values = [] self.latest_close_values = []
# Values of the last candle received # Values of the last candle received
self.last_candle = None self.last_candle = None
# List of dictionaries of timestamped volume values # List of dictionaries of timestamped volume values
self.latest_vol = [] self.latest_vol = []
@ -26,11 +32,15 @@ class Candles:
self.set_candle_history(symbol=config.trading_pair, interval=config.chart_interval) self.set_candle_history(symbol=config.trading_pair, interval=config.chart_interval)
def set_new_candle(self, cdata): 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.last_candle = cdata
self.latest_close_values.append({'time': cdata['time'], 'close': cdata['close']}) self.latest_close_values.append({'time': cdata['time'], 'close': cdata['close']})
self.latest_high_values.append({'time': cdata['time'], 'high': cdata['high']}) self.latest_high_values.append({'time': cdata['time'], 'high': cdata['high']})
self.latest_low_values.append({'time': cdata['time'], 'low': cdata['low']}) self.latest_low_values.append({'time': cdata['time'], 'low': cdata['low']})
self.latest_vol.append({'time': cdata['time'], 'value': cdata['vol']}) self.latest_vol.append({'time': cdata['time'], 'value': cdata['vol']})
return True
def load_candle_history(self, symbol, interval): def load_candle_history(self, symbol, interval):
""" Retrieve candlestick history from a file and append it with """ Retrieve candlestick history from a file and append it with
@ -76,7 +86,10 @@ class Candles:
# Set <last_candle_stamp> with the timestamp of the last candles on file # Set <last_candle_stamp> with the timestamp of the last candles on file
last_candle_stamp = candlesticks[-1][0] last_candle_stamp = candlesticks[-1][0]
# Request any missing candlestick data from the exchange # 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 # Discard the first row of candlestick data as it will be a duplicate***DOUBLE CHECK THIS
recent_candlesticks.pop(0) recent_candlesticks.pop(0)
# Append the candlestick list and the file # Append the candlestick list and the file

176
data.py
View File

@ -1,10 +1,7 @@
from binance.client import Client
import config
from Strategies import Strategies from Strategies import Strategies
from candles import Candles from candles import Candles
from Configuration import Configuration from Configuration import Configuration
from exchange_info import ExchangeInfo from exchange import Exchange
from indicators import Indicators from indicators import Indicators
from Signals import Signals from Signals import Signals
from trade import Trades from trade import Trades
@ -14,39 +11,34 @@ import json
class BrighterData: class BrighterData:
def __init__(self): def __init__(self):
# Initialise a connection to the Binance client API # Object that interacts and maintains exchange and account data
self.client = Client(config.API_KEY, config.API_SECRET) self.exchange = Exchange()
# Object that maintains signals
self.signals = Signals()
# Configuration and settings for the user interface and charts # Configuration and settings for the user interface and charts
self.config = Configuration() self.config = Configuration()
# Load any saved data from file # Object that maintains signals. Initialize with any signals loaded from file.
self.config.config_and_states('load') self.signals = Signals(self.config.signals_list)
# Initialize signals with loaded data.
self.signals.init_loaded_signals(self.config.signals_list)
# Object that maintains candlestick and price data. # 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 # Object that interacts with and maintains data from available indicators
self.indicators = Indicators(self.candles, self.config) self.indicators = Indicators(self.candles, self.config)
# Object that maintains exchange and account data # Object that maintains the trades data
self.exchange_info = ExchangeInfo(self.client) self.trades = Trades(self.config.trades)
# The Trades object needs to connect to an exchange.
# Object that maintains the strategies data self.trades.connect_exchange(exchange=self.exchange)
self.trades = Trades(self.client)
# Object that maintains the strategies data # Object that maintains the strategies data
self.strategies = Strategies(self.config.strategies_list, self.trades) self.strategies = Strategies(self.config.strategies_list, self.trades)
def get_js_init_data(self): 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, js_data = {'i_types': self.indicators.indicator_types,
'indicators': self.indicators.indicator_list, 'indicators': self.indicators.indicator_list,
'interval': self.config.chart_interval, 'interval': self.config.chart_interval,
@ -55,13 +47,13 @@ class BrighterData:
def get_rendered_data(self): 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 = {}
rd['title'] = self.config.application_title # Title of the page rd['title'] = self.config.application_title # Title of the page
rd['my_balances'] = self.exchange_info.balances # Balances on the exchange rd['my_balances'] = self.exchange.balances # Balances on the exchange
rd['symbols'] = self.exchange_info.symbols # Symbols information from the exchange rd['symbols'] = self.exchange.symbols # Symbols information from the exchange
rd['intervals'] = self.exchange_info.intervals # Time candle time intervals available to stream 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['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_types'] = self.indicators.indicator_types # All the types indicators Available
rd['indicator_list'] = self.indicators.get_indicator_list() # indicators available rd['indicator_list'] = self.indicators.get_indicator_list() # indicators available
@ -70,83 +62,96 @@ class BrighterData:
return rd return rd
def received_cdata(self, cdata): 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: <A candle object> - An object containing the most recent price data.
:return: <dict> - 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: if not self.candles.last_candle:
# Set last_candle.
self.candles.last_candle = cdata self.candles.last_candle = cdata
# If this candle is the same as last candle return nothing to do. # If this is not the first candle, but it's the same as the last candle recorded.
elif cdata['time']: elif cdata['time'] == self.candles.last_candle['time']:
if cdata['time'] == self.candles.last_candle['time']: # Return without doing anything.
return 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) self.candles.set_new_candle(cdata)
# Update the indicators and receive a dictionary of indicator results.
i_updates = self.indicators.update_indicators() 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) 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) 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) stg_updates = self.strategies.update(self.signals)
# Format and return an update object. # Format and return an update object to pass information to the frontend UI.
updates = {'i_updates': i_updates} updates = {}
if i_updates:
updates.update({'i_updates': i_updates})
if state_changes: if state_changes:
print(state_changes) print(state_changes)
updates.update({'s_updates': state_changes}) updates.update({'s_updates': state_changes})
if stg_updates:
print(stg_updates)
updates.update({'stg_updts': stg_updates})
if trade_updates: if trade_updates:
print(trade_updates) print(trade_updates)
updates.update({'trade_updts': trade_updates}) updates.update({'trade_updts': trade_updates})
if stg_updates:
print(stg_updates)
updates.update({'stg_updts': stg_updates})
return updates return updates
def received_new_signal(self, data): 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: <dict> - 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: if 'name' not in data:
return 'data.py:received_new_signal() - The new signal has no name. ' 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. # Forward the new signal data to the signals instance. So it can create a new signal.
self.signals.new_signal(data) self.signals.new_signal(data)
# Forward the new signal data to config. So it can save it to file. # Update config's list of signals and save it to file.
self.config.new_signal(data) self.config.update_data('signals', self.signals.get_signals('dict'))
# Send the data back to where it came from. # Send the data back to where it came from.
return data return data
def received_new_strategy(self, 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: <dict> - 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: if 'name' not in data:
return 'data.py:received_new_strategy() - The new strategy has no name. ' 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. # Forward the new strategy data to the strategy's instance. So it can create a new strategy.
self.strategies.new_strategy(data) self.strategies.new_strategy(data)
# Forward the new signal data to config. So it can save it to file. # Update config's list of strategies and save to file.
self.config.new_strategy(data) self.config.update_data('strategies', self.strategies.get_strategies('dict'))
# Send the data back to where it came from. # Send the data back to where it came from.
return data return data
def delete_strategy(self, strategy_name): 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) 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) self.config.remove('strategies', strategy_name)
def get_signals(self): def get_signals(self):
""" Return a JSON object of all the signals in the signals instance.""" """Return a JSON object of all the signals in the signals instance."""
sigs = self.signals.get_signals() return self.signals.get_signals('json')
json_str = []
for sig in sigs:
json_str.append(json.dumps(sig.__dict__))
return json_str
def get_strategies(self): def get_strategies(self):
""" Return a JSON object of all the signals in the signals instance.""" """ Return a JSON object of all the strategies in the strategies instance."""
strats = self.strategies.get_strategies() return self.strategies.get_strategies('json')
json_str = []
for strat in strats:
json_str.append(json.dumps(strat))
return json_str
def delete_signal(self, signal_name): def delete_signal(self, signal_name):
# Delete the signal from the signals instance. # Delete the signal from the signals instance.
@ -154,3 +159,48 @@ class BrighterData:
# Delete the signal from the configuration file. # Delete the signal from the configuration file.
self.config.remove('signals', signal_name) 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: <dict> - 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')

93
exchange.py Normal file
View File

@ -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'])

View File

@ -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
)

View File

@ -398,25 +398,28 @@ class Indicators:
enabled_indicators.append(indctr) enabled_indicators.append(indctr)
return enabled_indicators 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 # Loop through all the indicators. If enabled, run the appropriate
# update function. Return all the results as a dictionary object. # update function. Return all the results as a dictionary object.
if symbol is not None: if symbol is not None:
print(symbol) if self.candles.config.trading_pair != symbol:
print('get_indicator_data() no symbol implementation') print(f'get_indicator_data: request candle data for {symbol}')
if interval is not None: if interval is not None:
print(interval) if self.candles.config.chart_interval != interval:
print('get_indicator_data() no interval implementation') 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. # Get a list of indicator objects and a list of enabled indicators names.
i_list = self.indicators i_list = self.indicators
enabled_i = self.get_enabled_indicators() enabled_i = self.get_enabled_indicators()
candles = self.candles
result = {} result = {}
# Loop through all indicator objects in i_list # Loop through all indicator objects in i_list
for each_i in i_list: for each_i in i_list:
# If the indicator's not enabled skip to next each_i # If the indicator's not enabled skip to next each_i
if each_i not in enabled_i: if each_i not in enabled_i:
continue 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 return result
def delete_indicator(self, indicator): def delete_indicator(self, indicator):

View File

@ -2,4 +2,5 @@ numpy~=1.22.3
flask~=2.1.2 flask~=2.1.2
config~=0.5.1 config~=0.5.1
PyYAML~=6.0 PyYAML~=6.0
binance binance~=0.3
requests~=2.27.1

View File

@ -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 { class Strategies {
constructor(target_id) { constructor(target_id) {
// The list of strategies. // The list of strategies.
@ -47,10 +16,10 @@ class Strategies {
submit(){ submit(){
/* /*
- Collect the data from the form fields and - Collect the data from the form fields and
create an object representing the strategy. create a json object representing a strategy.
- Record the strategy in the an instance list. - Append the strategy to a local list of strategies.
- Update the display output. - Update the display output.
- Send the strategy object to the server. - Send the strategy info to the server.
*/ */
let strat = {}; let strat = {};
strat.name = document.getElementById('stg_name').value; strat.name = document.getElementById('stg_name').value;
@ -58,27 +27,41 @@ class Strategies {
alert('Please provide a name.'); alert('Please provide a name.');
return; return;
} }
// The type of strategy will determine underlying feature of its execution.
strat.type = document.getElementById('strat_type').value; strat.type = document.getElementById('strat_type').value;
// Whether the strategy is bullish or bearish.
strat.side = document.getElementById('trade_in_side').value; 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; strat.trade_amount = document.getElementById('trade_amount').value;
// The maximum combined position allowed.
strat.max_position = document.getElementById('strgy_total').value; strat.max_position = document.getElementById('strgy_total').value;
// The fee the exchange charges per trade.
strat.trading_fee = document.getElementById('fee').value; 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; strat.max_loss = document.getElementById('max_loss').value;
// The trading pair being exchanged.
strat.symbol = window.UI.data.trading_pair; strat.symbol = window.UI.data.trading_pair;
strat.net_profit = 0; // The combined profit or loss including trading fees.
strat.gross_profit = 0; strat.net_pl = 0;
strat.net_loss = 0; // The combined profit or loss.
strat.gross_loss = 0; strat.gross_pl = 0;
strat.current_position = 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; strat.current_value = 0;
// Whether or not the strategy has begun trading.
strat.active = false; strat.active = false;
// The conditions that must be met before trading in.
strat.trd_in_conds = {}; strat.trd_in_conds = {};
let conds = Array.from(document.querySelectorAll('#trade_in_cond>li')); let conds = Array.from(document.querySelectorAll('#trade_in_cond>li'));
for (let cond of conds){ for (let cond of conds){
let json_obj = JSON.parse(cond.innerHTML); let json_obj = JSON.parse(cond.innerHTML);
strat.trd_in_conds[json_obj.Trigger] = json_obj.Value; strat.trd_in_conds[json_obj.Trigger] = json_obj.Value;
} }
// The conditions that must be met before taking profit.
let take_profit = {}; let take_profit = {};
take_profit.typ = document.getElementById('prof_typ').value; take_profit.typ = document.getElementById('prof_typ').value;
if (take_profit.typ == 'conditional'){ if (take_profit.typ == 'conditional'){
@ -88,6 +71,8 @@ class Strategies {
take_profit.val = document.getElementById('prof_val').value; take_profit.val = document.getElementById('prof_val').value;
} }
strat.take_profit = take_profit; strat.take_profit = take_profit;
// The conditions that must be met before taking a loss.
let stop_loss = {}; let stop_loss = {};
stop_loss.typ = document.getElementById('loss_typ').value; stop_loss.typ = document.getElementById('loss_typ').value;
if ( stop_loss.typ == 'conditional' ){ if ( stop_loss.typ == 'conditional' ){
@ -97,12 +82,17 @@ class Strategies {
stop_loss.val = document.getElementById('loss_val').value; stop_loss.val = document.getElementById('loss_val').value;
} }
strat.stop_loss = stop_loss; strat.stop_loss = stop_loss;
// Add the strategy to the instance list. // Add the strategy to the instance list.
this.strategies.push(strat); this.strategies.push(strat);
// Add the strategy to display. // Add the strategy to display.
this.update_html(); this.update_html();
// Send the new strategy to the server. // Send the new strategy to the server.
window.UI.data.comms.send_to_app( "new_strategy", strat); window.UI.data.comms.send_to_app( "new_strategy", strat);
// Close the html form.
this.close_form(); this.close_form();
} }
update_received(stg_updts){ update_received(stg_updts){
@ -124,7 +114,7 @@ class Strategies {
} }
open_stg_form(){ open_stg_form(){
this.open_form(); this.open_form();
this.fill_field('strat_opt', 'take_profit'); this.fill_field('strat_opt', 'in-out');
} }
clear_innerHTML(el){ clear_innerHTML(el){
el.innerHTML=""; el.innerHTML="";
@ -190,7 +180,7 @@ class Strategies {
if (field == 'strat_opt'){ if (field == 'strat_opt'){
let options = document.getElementById('strat_opt'); let options = document.getElementById('strat_opt');
if (value == 'take_profit'){ if (value == 'in-out'){
// Clear previous content. // Clear previous content.
this.clear_innerHTML(options); this.clear_innerHTML(options);

6
static/backtesting.js Normal file
View File

@ -0,0 +1,6 @@
class Backtesting {
constructor() {
this.height = height;
}
}

View File

@ -149,10 +149,10 @@ input[type="checkbox"] {
#chart_controls{ #chart_controls{
border-style:none; border-style:none;
width: 200px; width: 775px;
padding: 15px; padding: 15px;
display: grid; display: grid;
grid-template-columns:700px 1fr 1fr; grid-template-columns:350px 2fr 1fr 1fr;
} }
#indicators{ #indicators{
@ -186,7 +186,6 @@ input[type="checkbox"] {
} }
/***************************************************/ /***************************************************/
/****************Right Panel***********************/ /****************Right Panel***********************/
.collapsible { .collapsible {
background-color: #3E3AF2; background-color: #3E3AF2;
@ -272,9 +271,24 @@ position: absolute;
.new_btn{ .new_btn{
margin:5; 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/Add Indicators Panel**************/
#edit_indcr_panel{ #edit_indcr_panel{
width: 1000px; width: 1000px;

View File

@ -1,27 +1,54 @@
class Comms { class Comms {
constructor(ocu, occ, oiu) { constructor() {
// Register callbacks // List of callbacks callback function that will receive various updates.
this.on_candle_update = ocu; this.on_candle_update = [];
this.on_candle_close = occ; this.on_candle_close = [];
this.on_indctr_update = oiu; this.on_indctr_update = [];
// Status of connection.
this.connection_open = false; 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){ candle_update(new_candle){
// Call the callback provided. // If no callback is registered do nothing.
this.on_candle_update(new_candle); 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){ candle_close(new_candle){
// Forward a copy of the new candle to the server. // Forward a copy of the new candle to the server.
this.app_con.send( JSON.stringify({ message_type: "candle_data", data :new_candle })); 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){ indicator_update(data){
// Call the callback provided. // If no callback is registered do return.
this.on_indctr_update(data); 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(){ getPriceHistory(){
@ -113,8 +140,9 @@ class Comms {
}) })
} }
set_exchange_con(interval){ set_exchange_con(interval, tradingPair){
let ws = "wss://stream.binance.com:9443/ws/btcusdt@kline_" + interval; tradingPair = tradingPair.toLowerCase();
let ws = "wss://stream.binance.com:9443/ws/" + tradingPair + "@kline_" + interval;
this.exchange_con = new WebSocket(ws); this.exchange_con = new WebSocket(ws);
// Set the on-message call-back for the socket // Set the on-message call-back for the socket

15
static/controls.js vendored
View File

@ -26,5 +26,20 @@ class Controls {
el.style.top = y + "px"; el.style.top = y + "px";
el.style.display = "block"; 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();
}
}
};
}
} }

View File

@ -14,18 +14,24 @@ class Data {
// All the indicators available. // All the indicators available.
this.indicators = bt_data.indicators; this.indicators = bt_data.indicators;
/* Comms handles communication with the servers. Pass it /* Comms handles communication with the servers. Register
a list of callbacks to handle various incoming messages.*/ callbacks to handle various incoming messages.*/
this.comms = new Comms(this.candle_update, this.candle_close, this.indicator_update); 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. // Open the connection to our local server.
this.comms.set_app_con(); this.comms.set_app_con();
/* Open connection for streaming candle data wth the exchange. /* Open connection for streaming candle data wth the exchange.
Pass it the time period of candles to stream. */ 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. //Request historical price data from the server.
this.price_history = this.comms.getPriceHistory(); 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. // Request from the server initialization data for the indicators.
this.indicator_data = this.comms.getIndicatorData(); this.indicator_data = this.comms.getIndicatorData();
@ -37,7 +43,7 @@ class Data {
// This is called everytime a candle update comes from the local server. // This is called everytime a candle update comes from the local server.
window.UI.charts.update_main_chart(new_candle); window.UI.charts.update_main_chart(new_candle);
//console.log('Candle update:'); //console.log('Candle update:');
//console.log(new_candle); this.last_price = new_candle.close;
} }
set_i_updates(call_back){ set_i_updates(call_back){
this.i_updates = call_back; this.i_updates = call_back;

View File

@ -1,15 +1,3 @@
//
//class Backtesting {
// constructor() {
// this.height = height;
// }
//}
//
//class Trade {
// constructor() {
// this.height = height;
// }
//}
//class Exchange_Info { //class Exchange_Info {
// constructor() { // constructor() {
// this.height = height; // this.height = height;
@ -21,14 +9,6 @@
// this.height = height; // this.height = height;
// } // }
//} //}
//
//class Statistics {
// constructor() {
// this.height = height;
// }
//}
//
class User_Interface{ class User_Interface{
/* This contains all the code for our User interface. /* This contains all the code for our User interface.
@ -52,11 +32,17 @@ class User_Interface{
this.alerts = new Alerts("alert_list"); this.alerts = new Alerts("alert_list");
/* The object that handles alerts. Pass in the html /* 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"); this.strats = new Strategies("strats_display");
/* These classes interact with HTML elements that need to be parsed first. /* The object that handles trades. Pass in the html
TODO: Can any of these objects be created then run init functions after loading?*/ 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 () { window.addEventListener('load', function () {
/* Charts object is responsible for maintaining the /* Charts object is responsible for maintaining the
data visualisation area in the UI. */ data visualisation area in the UI. */
@ -87,6 +73,9 @@ class User_Interface{
window.UI.alerts.set_target(); window.UI.alerts.set_target();
// initialize the strategies instance. // initialize the strategies instance.
window.UI.strats.initialize(); window.UI.strats.initialize();
// initialize the controls instance.
window.UI.controls.init_TP_selector();
window.UI.trade.initialize();
}); });
} }

View File

@ -3,30 +3,36 @@ class Signals {
this.indicators = indicators; this.indicators = indicators;
this.signals=[]; 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"; } 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"; } close_signal_Form() { document.getElementById("new_sig_form").style.display = "none"; }
request_signals(){ request_signals(){
// Requests a list of all the signals from the server.
window.UI.data.comms.send_to_app('request', 'signals'); window.UI.data.comms.send_to_app('request', 'signals');
} }
delete_signal(signal_name){ delete_signal(signal_name){
// Requests that the server remove a specific signal.
window.UI.data.comms.send_to_app('delete_signal', signal_name); 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'); 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); child.parentNode.removeChild(child);
} }
i_update(updates){ 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){ 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]; 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); this.signals[signal].value1 = value1.toFixed(2);
if (this.signals[signal].source2 != 'value'){ 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]; let value2 = updates[this.signals[signal].source2].data[0][this.signals[signal].prop2];
this.signals[signal].value2 = value2.toFixed(2); 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 + '_value1').innerHTML = this.signals[signal].value1;
document.getElementById(this.signals[signal].name + '_value2').innerHTML = this.signals[signal].value2; 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. */ than passing functions, references and callbacks around. */
window.UI.data.comms.send_to_app( "new_signal", data); window.UI.data.comms.send_to_app( "new_signal", data);
this.close_signal_Form();
} }
} }

5
static/statistics.js Normal file
View File

@ -0,0 +1,5 @@
class Statistics {
constructor() {
this.height = height;
}
}

58
static/trade.js Normal file
View File

@ -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();
}
}

View File

@ -19,12 +19,61 @@
<script src="{{ url_for('static', filename='communication.js') }}"></script> <script src="{{ url_for('static', filename='communication.js') }}"></script>
<script src="{{ url_for('static', filename='controls.js') }}"></script> <script src="{{ url_for('static', filename='controls.js') }}"></script>
<script src="{{ url_for('static', filename='signals.js') }}"></script> <script src="{{ url_for('static', filename='signals.js') }}"></script>
<script src="{{ url_for('static', filename='trade.js') }}"></script>
<script src="{{ url_for('static', filename='general.js') }}"></script> <script src="{{ url_for('static', filename='general.js') }}"></script>
</head> </head>
<body> <body>
<!-- Pop up forms for interface --> <!-- Pop up forms for interface -->
<div class="form-popup" id="new_trade_form">
<form action="/new_trade" class="form-container">
<!-- Panel 1 of 1 (8 rows, 4 columns) -->
<div id="trade_pan_1" class="form_panels" style="display: grid;grid-template-columns:repeat(4,1fr);grid-template-rows: repeat(8,1fr);">
<!-- Panel title (row 1/8)-->
<h1 style="grid-column: 1 / span 2; grid-row: 1;">Create New Trade</h1>
<!-- Target input field (row 2/8)-->
<div id = "trade_target_div" style="grid-column: 1 / span 2; grid-row:2;">
<label for='trade_target' >Trade target:</label>
<select name="trade_target" id="trade_target" style="grid-column: 2; grid-row: 2;" >
<option>backtester</option>
<option>exchange</option>
</select>
</div>
<!-- Side Input field (row 3/8)-->
<label for="side" style="grid-column: 1; grid-row: 3;"><b>Side:</b></label>
<select name="side" id="side" style="grid-column: 2; grid-row: 3;" >
<option>buy</option>
<option>sell</option>
</select>
<!-- orderType Input field (row 4/8)-->
<label for="orderType" style="grid-column: 1; grid-row: 4;"><b>Order type:</b></label>
<select name="orderType" id="orderType" style="grid-column: 2; grid-row: 4;" >
<option>MARKET</option>
<option>LIMIT</option>
</select>
<!-- Price Input field (row 5/8)-->
<label id="price-label" for="price" style="grid-column: 1; grid-row: 5;"><b>Price:</b></label>
<input type="number" min="0" value="0.1" step="0.1" name="price" id="price" style="grid-column: 2; grid-row: 5;" >
<output name="current_price" id="current_price" style="grid-column: 2; grid-row: 5;" >
</output>
<!-- quantity Input field (row 6/8)-->
<label for="quantity" style="grid-column: 1; grid-row: 6;"><b>Quantity:</b></label>
<input type="number" min="0" value="0" step="0.01" name="quantity" id="quantity" style="grid-column: 2; grid-row: 6;" >
</input>
<!-- Value field (row 7/8)-->
<label for="trade_value" style="grid-column: 1; grid-row: 7;"><b>Value</b></label>
<output name="trade_value" id="trade_value" for="quantity price" style="grid-column: 2; grid-row: 7;"></output>
<!-- buttons (row 8/8)-->
<div style="grid-column: 1 / span 4; grid-row: 8;">
<button type="button" class="btn cancel" onclick="UI.trade.close_tradeForm()">Close</button>
<button type="button" class="btn next" onclick="UI.trade.submitNewTrade()">Create Trade</button>
</div>
</div><!----End panel 1--------->
</form>
</div>
<div class="form-popup" id="new_strat_form"> <div class="form-popup" id="new_strat_form">
<form action="/new_strategy" class="form-container"> <form action="/new_strategy" class="form-container">
<!-- Panel 1 of 1 (5 rows, 2 columns) --> <!-- Panel 1 of 1 (5 rows, 2 columns) -->
@ -39,7 +88,7 @@
<!-- Source Input field (row 3/5)--> <!-- Source Input field (row 3/5)-->
<label for="strat_type" style="grid-column: 1; grid-row: 3;"><b>Strategy type</b></label> <label for="strat_type" style="grid-column: 1; grid-row: 3;"><b>Strategy type</b></label>
<select name="strat_type" id="strat_type" style="grid-column: 2; grid-row: 3;" onchange= "UI.strats.fill_field('strat_opt', this.value)"> <select name="strat_type" id="strat_type" style="grid-column: 2; grid-row: 3;" onchange= "UI.strats.fill_field('strat_opt', this.value)">
<option>take_profit</option> <option>in-out</option>
<option>incremental_profits</option> <option>incremental_profits</option>
<option>swing</option> <option>swing</option>
</select> </select>
@ -111,7 +160,7 @@
<input type="radio" id="lt" name="Operator" value="<"> <input type="radio" id="lt" name="Operator" value="<">
<label for = "lt">lessthan</label><br> <label for = "lt">lessthan</label><br>
<input type = "radio" id="eq" name="Operator" value="=" checked="checked"> <input type = "radio" id="eq" name="Operator" value="==" checked="checked">
<label for = "eq">Equals</label> <label for = "eq">Equals</label>
<input type="radio" id="within" name="Operator" value="+/-"> <input type="radio" id="within" name="Operator" value="+/-">
@ -199,6 +248,17 @@
<div id="chart_controls"> <div id="chart_controls">
<!-- Container target for any indicator output --> <!-- Container target for any indicator output -->
<div id="indicator_output" ></div> <div id="indicator_output" ></div>
<!-- Trading pair selector -->
<form id="tp_selector" action="/settings" method="post">
<input type="hidden" name="setting" value="trading_pair" />
<label for="symbols" >Trading Pair</label>
<input list="symbols" name="symbol" id="symbol" style="width: 96px;">
<datalist id="symbols">
{% for symbol in symbols %}
<option>{{ symbol['symbol'] }}</option>
{% endfor %}
</datalist>
</form>
<!-- Time interval selector --> <!-- Time interval selector -->
<form action="/settings" method="post"> <form action="/settings" method="post">
<input type="hidden" name="setting" value="interval" /> <input type="hidden" name="setting" value="interval" />
@ -288,16 +348,7 @@
<button class="collapsible bg_blue">Trade</button> <button class="collapsible bg_blue">Trade</button>
<div class="content"> <div class="content">
<div id="trade_content" class="cp_content"> <div id="trade_content" class="cp_content">
<form action="/trade" method="post"> <button class="new_btn" id="new_trade" onclick="UI.trade.open_tradeForm()">New Trade</button>
<input type="text" id="quantity" name="quantity" placeholder="eg. 0.001" />
<select id="symbol" name="symbol">
{% for symbol in symbols %}
<option>{{ symbol['symbol'] }}</option>
{% endfor %}
</select>
<input type="submit" name="buy" value="buy" />
<input type="submit" name="sell" value="sell" />
</form>
</div> </div>
</div> </div>

163
test_trade.py Normal file
View File

@ -0,0 +1,163 @@
from trade import Trade
def test_get_position_size():
opening_price = 1000
quantity = 0.01
trade_obj = Trade(symbol='BTCUSD', side='BUY', price=opening_price, qty=quantity, order_type='MARKET', tif='GTC',
unique_id=None, status=None, stats=None, order=None)
# Position size after construction.
position_size = trade_obj.get_position_size()
print('\nPosition size after construction.')
print(f'Trade for {quantity} BTC @ {opening_price} of the quote currency. Price has not changed.')
print(f'Position size: {position_size}')
assert position_size == opening_price * quantity
# Position size after current price changes on unopened trade.
print('\nPosition size after current price changes on unopened trade.')
current_price = 50
result = trade_obj.update(current_price)
print(f'the result of the update {result}')
position_size = trade_obj.get_position_size()
print(f'{quantity} BTC bought at {opening_price} is current priced at {current_price}')
print(f'Position size: {position_size}')
assert position_size == opening_price * quantity
print('\nPosition size after trade fills at opening price.')
# Position size after trade fills
trade_obj.trade_filled(quantity, opening_price)
position_size = trade_obj.get_position_size()
print(f'{quantity} BTC bought at {opening_price} is current priced at {current_price}')
print(f'Position size: {position_size}')
assert position_size == opening_price * quantity
print('\nPosition size after current price changes on live trade.')
# Position size after current price changes on open trade
current_price = 50
result = trade_obj.update(current_price)
print(f'the result of the update {result}')
position_size = trade_obj.get_position_size()
print(f'{quantity} BTC bought at {opening_price} is current priced at {current_price}')
print(f'Position size: {position_size}')
assert position_size == current_price * quantity
def test_update_values():
# Create a test trade.
opening_price = 100
quantity = 0.1
trade_obj = Trade(symbol='BTCUSD', side='BUY', price=opening_price, qty=quantity, order_type='MARKET', tif='GTC',
unique_id=None, status=None, stats=None, order=None)
trade_obj.trade_filled(quantity, opening_price)
print(f'\nopening price is {opening_price}')
# Output the pl for unchanged trade.
print('\n Live trade price has not changed.')
position_size = trade_obj.get_position_size()
print(f'Position size: {position_size}')
assert position_size == 10
pl = trade_obj.get_pl()
print(f'PL reported: {pl}')
assert pl == 0
pl_pct = trade_obj.get_pl_pct()
print(f'PL% reported: {pl_pct}')
assert pl_pct == 0
# Divide the price of the quote asset by 2.
current_price = 50
trade_obj.update_values(current_price)
# Output PL for adjusted trade.
print(f'\n Live trade price has changed to {current_price}.')
position_size = trade_obj.get_position_size()
print(f'Position size: {position_size}')
assert position_size == 5
pl = trade_obj.get_pl()
print(f'PL reported: {pl}')
# Should be: - 1/2 * opening value - opening value * fee - closing value * fee
# 5 - 1 - 0.5 = -6.5
assert pl == -6.5
pl_pct = trade_obj.get_pl_pct()
print(f'PL% reported: {pl_pct}')
# Should be -6.5/10 = -65%
assert pl_pct == -65
# Add 1/2 to the price of the quote asset.
current_price = 150
trade_obj.update_values(current_price)
# Output PL for adjusted trade.
print(f'\n Live trade price has changed to {current_price}.')
position_size = trade_obj.get_position_size()
print(f'Position size: {position_size}')
assert position_size == 15
pl = trade_obj.get_pl()
print(f'PL reported: {pl}')
# Should be 5 - opening fee - closing fee
# fee should be (10 * .1) + (15 * .1) = 2.5
assert pl == 2.5
pl_pct = trade_obj.get_pl_pct()
print(f'PL% reported: {pl_pct}')
# should be 2.5/10 = 25%
assert pl_pct == 25
def test_update():
# Create test trade.
opening_price = 1000
quantity = 0.01
trade_obj = Trade(symbol='BTCUSD', side='BUY', price=opening_price, qty=quantity, order_type='MARKET', tif='GTC',
unique_id=None, status=None, stats=None, order=None)
print('\nUpdate price of un-placed trade.')
# Use Update to change the price of the inactive trade.
current_price = 50
result = trade_obj.update(current_price)
print(f'The result {result}')
assert result == 'trade_not_placed'
# Simulate a placed trade.
trade_obj.trade_filled(0.01, 1000)
print('\nUpdate price of live trade.')
# Use Update to change the price.
current_price = 50
result = trade_obj.update(current_price)
print(f'The result {result}')
assert result == 'trade_updated'
position_size = trade_obj.get_position_size()
print(f'\n{quantity} BTC bought at {opening_price} is current priced at {current_price}')
print(f'Position size: {position_size}')
assert position_size == current_price * quantity
def test_trade_filled():
# Create a test trade.
opening_price = 100
quantity = 0.1
trade_obj = Trade(symbol='BTCUSD', side='BUY', price=opening_price, qty=quantity, order_type='MARKET', tif='GTC',
unique_id=None, status=None, stats=None, order=None)
trade_obj.trade_filled(qty=0.05, price=100)
status = trade_obj.get_status()
print(f'\n Status after trade_filled() called: {status}')
assert status == 'part_filled'
trade_obj.trade_filled(qty=0.05, price=100)
status = trade_obj.get_status()
print(f'\n Status after trade_filled() called: {status}')
assert status == 'filled'
def test_settle():
# Create a test trade.
opening_price = 100
quantity = 0.1
trade_obj = Trade(symbol='BTCUSD', side='BUY', price=opening_price, qty=quantity, order_type='MARKET', tif='GTC',
unique_id=None, status=None, stats=None, order=None)
trade_obj.trade_filled(qty=quantity, price=opening_price)
print(f'\ninitial status: {trade_obj.get_status()}')
print('settle the trade at 120')
trade_obj.settle(qty=0.1, price=120)
status = trade_obj.get_status()
print(f'final status: {status}')
assert status == 'closed'

0
tests/__init__.py Normal file
View File

241
tests/test_trade.py Normal file
View File

@ -0,0 +1,241 @@
from exchange import Exchange
from trade import Trades
def test_connect_exchange():
exchange = Exchange()
test_trades_obj = Trades()
test_trades_obj.connect_exchange(exchange)
print(f'\nConnected to exchange: {test_trades_obj.exchange_connected()}')
account_info = exchange.get_precision(symbol='ETHUSDT')
print(account_info)
assert test_trades_obj.exchange_connected()
def test_get_trades_by_status():
# Connect to the exchange
exchange = Exchange()
test_trades_obj = Trades()
test_trades_obj.connect_exchange(exchange)
print(f'\nConnected to exchange: {test_trades_obj.exchange_connected()}')
assert test_trades_obj.exchange_connected()
# create a trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 1.5, price=100, offset=None)
print('trade 0 created.')
print(test_trades_obj.active_trades[0].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[0].status is 'inactive'
# create a 2nd trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
print('trade 1 created.')
print(test_trades_obj.active_trades[1].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[1].status is 'inactive'
# create a 3rd trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
print('trade 2 created.')
print(test_trades_obj.active_trades[2].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[1].status is 'inactive'
# should be three trades in this list
print(f'Expecting 3 trades in list: Actual:{len(test_trades_obj.active_trades)}')
assert len(test_trades_obj.active_trades) is 3
print(test_trades_obj.active_trades[0].status)
print(test_trades_obj.active_trades[1].status)
print(test_trades_obj.active_trades[2].status)
# fill trade one
test_trades_obj.active_trades[1].trade_filled(0.4, 2100)
print(f'trade 1 filled. status:')
print(test_trades_obj.active_trades[1].status)
# Search for all inactive trades
result = test_trades_obj.get_trades_by_status('inactive')
print(f'search for all inactive trades. The result: {result}')
assert len(result) is 2
# Search for all filled trades
result = test_trades_obj.get_trades_by_status('filled')
print(f'search for all filled trades. The result: {result}')
assert len(result) is 1
def test_get_trade_by_id():
# Connect to the exchange
exchange = Exchange()
test_trades_obj = Trades()
test_trades_obj.connect_exchange(exchange)
print(f'\nConnected to exchange: {test_trades_obj.exchange_connected()}')
assert test_trades_obj.exchange_connected()
# create a trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 1.5, price=100, offset=None)
print('trade 0 created.')
print(test_trades_obj.active_trades[0].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[0].status is 'inactive'
# create a 2nd trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
print('trade 1 created.')
print(test_trades_obj.active_trades[1].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[1].status is 'inactive'
id = test_trades_obj.active_trades[0].unique_id
print(f'the id of trade 0 is{id}')
result = test_trades_obj.get_trade_by_id(id)
print(f'here is the result after searching for the id:{result}')
assert result.unique_id is id
def test_load_trades():
assert False
def test_place_order():
# Connect to the exchange
exchange = Exchange()
test_trades_obj = Trades()
test_trades_obj.connect_exchange(exchange)
print(f'\nConnected to exchange: {test_trades_obj.exchange_connected()}')
# Create a new treade on the exchange.
test_trades_obj.new_trade('exchange', 'BTCUSDT', 'BUY', 'MARKET', 0.1, price=100, offset=None)
print(test_trades_obj.active_trades[0].__dict__)
# If the status of the trade is unfilled the order is placed.
assert test_trades_obj.active_trades[0].status is 'unfilled'
def test_update():
# Connect to the exchange
exchange = Exchange()
test_trades_obj = Trades()
test_trades_obj.connect_exchange(exchange)
print(f'\nConnected to exchange: {test_trades_obj.exchange_connected()}')
# Create a trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=100, offset=None)
print(test_trades_obj.active_trades[0].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[0].status is 'inactive'
# create a 2nd trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.2, price=100, offset=None)
print(test_trades_obj.active_trades[1].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[1].status is 'inactive'
test_trades_obj.active_trades[0].trade_filled(0.4, 100)
test_trades_obj.active_trades[1].trade_filled(0.2, 100)
test_trades_obj.update(200)
print(test_trades_obj.active_trades[0].__dict__)
print(test_trades_obj.active_trades[1].__dict__)
def test_new_trade():
# Connect to the exchange
exchange = Exchange()
test_trades_obj = Trades()
test_trades_obj.connect_exchange(exchange)
print(f'\nConnected to exchange: {test_trades_obj.exchange_connected()}')
# create an trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.1, price=100, offset=None)
print(test_trades_obj.active_trades[0].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[0].status is 'inactive'
# create a 2nd trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
print(test_trades_obj.active_trades[1].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[1].status is 'inactive'
def test_close_trade():
# Connect to the exchange
exchange = Exchange()
test_trades_obj = Trades()
test_trades_obj.connect_exchange(exchange)
print(f'\nConnected to exchange: {test_trades_obj.exchange_connected()}')
# create a trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.1, price=100, offset=None)
print(test_trades_obj.active_trades[0].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[0].status is 'inactive'
# create a 2nd trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
print(test_trades_obj.active_trades[1].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[1].status is 'inactive'
# should be two trades in this list
print(len(test_trades_obj.active_trades))
assert len(test_trades_obj.active_trades) > 1
trade_id = test_trades_obj.active_trades[0].unique_id
test_trades_obj.close_trade(trade_id)
# should be 1 trade in this list
print(len(test_trades_obj.active_trades))
assert len(test_trades_obj.active_trades) == 1
def test_reduce_trade():
# Connect to the exchange
exchange = Exchange()
test_trades_obj = Trades()
test_trades_obj.connect_exchange(exchange)
print(f'\nConnected to exchange: {test_trades_obj.exchange_connected()}')
assert test_trades_obj.exchange_connected()
# create a trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 1.5, price=100, offset=None)
print('trade 0 created.')
print(test_trades_obj.active_trades[0].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[0].status is 'inactive'
# create a 2nd trade but not on the exchange
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
print('trade 1 created.')
print(test_trades_obj.active_trades[1].__dict__)
# If the status of the trade is inactive the trade is created but the order isn't placed.
assert test_trades_obj.active_trades[1].status is 'inactive'
# should be two trades in this list
print(f'Expecting 2 trades in list: Actual:{len(test_trades_obj.active_trades)}')
assert len(test_trades_obj.active_trades) > 1
# Grab inactive trade 0
trade_id = test_trades_obj.active_trades[0].unique_id
# reduce the trade by 0.5
remaining_qty = test_trades_obj.reduce_trade(trade_id, 0.5)
# The trade should be 1 (1.5 - 0.5)
print(f'The remaining quantity of the trade should be 1: Actual: {remaining_qty}')
assert remaining_qty == 1
print('trade 0:')
print(test_trades_obj.active_trades[0].__dict__)
test_trades_obj.active_trades[1].trade_filled(0.4, 2100)
# Grab filled trade 1
trade_id = test_trades_obj.active_trades[1].unique_id
# reduce the trade by 0.1
remaining_qty = float(test_trades_obj.reduce_trade(trade_id, 0.1))
# The trade should be 0.3 (0.4 - 0.1)
print(f'\nThe remaining quantity of trade 1 should be 0.3: Actual: {remaining_qty}')
assert remaining_qty == 0.3
print('trade 1:')
print(test_trades_obj.active_trades[1].__dict__)

798
trade.py
View File

@ -1,200 +1,694 @@
import json import json
import uuid
import requests import requests
from datetime import datetime from datetime import datetime
class Trade: class Trade:
def __init__(self, symbol, side, order_type, position_size, price): def __init__(self, dest, symbol, side, price, qty, order_type='MARKET', tif='GTC', unique_id=None, status=None,
stats=None, order=None, fee=0.1):
"""
This object contains the information,value and statistics for individual active_trades.
Flags and stats can be set in the constructor in order to load previous active_trades.
Creating a trade does not execute it on the exchange.
:param dest: <str> - ['exchange'|'backtester'] The destination for this trade.
:param symbol: <str> - The symbol of the trading pair.
:param side: <str> - BUY|SELL
:param price: <float> - Asking price for LIMIT orders.
:param qty: <float> - Base quantity to buy
:param order_type: <str> - LIMIT|MARKET
:param tif: <str> - ['GTC'|'IOC'|'FOK'] Time-in-force. Defaults to GTC. This flag is used externally.
:param unique_id <hex> - Identifier
:param status: <str> ['inactive'|'unfilled'|'filled'|'part-filled'|'closed'] - order status.
:param stats: <dic> ['opening_price']:float | ['current_price']:float | ['opening_value']:float |
['current_value']:float | ['profit'] | ['profit_pct']:float
- Price and P/L values and percentages for the trade.
:param fee: <float> - the fee rate charged when trading in or out.
"""
# Set the ID or generate one if not provided.
# Trade ID should be the same as order id.
if unique_id is None:
self.unique_id = uuid.uuid4().hex
else:
self.unique_id = unique_id
# Store the destination for the trade. ['exchange'|'backtester']
self.dest = dest
# Set the symbol of the pair being traded.
self.symbol = symbol
# Set the side - buy or sell
self.side = side
# Set the order_type. Defaults to 'MARKET'. This flag is used externally when placing the order.
# LIMIT: Places an order at a set asking price.
# MARKET: Accept an offer off the table.
# TRAILING: Follow the price with LIMIT orders a set value
# from the price until it fills.
self.order_type = order_type
# Set Time-in-force. Defaults to GTC. This flag is used externally.
# GTC (Good-Till-Cancel) - leave it on the orderbook.
# IOC (Immediate-Or-Cancel) - if it can't at least partially fill immediately cancel the order.
# FOK (Fill-Or-Kill) - If it can't fully fill immediately cancel the order.
self.time_in_force = tif
# Set the quantity to be traded.
self.base_order_qty = qty
# Set the fee rate the exchange is charging.
self.fee = fee
# If status is None this is a new trade. Set the default
# values.
if status is None:
# Trade status flag. ['inactive'|'unfilled'|'filled'|'part_filled'|'closed']
self.status = 'inactive'
# Trade statistics.
self.stats = {
'opening_price': price,
'opening_value': (qty * price),
'current_price': price,
'current_value': (qty * price),
'settled_price': 0,
'settled_value': 0,
'qty_filled': 0,
'qty_settled': 0,
'profit': 0,
'profit_pct': 0,
'fee_paid': 0
}
else:
# Loading an existing trade. Set the values.
self.status = status
self.stats = stats
self.order = order
def get_position_size(self):
"""
Position_size is the value of the trade in the quote currency.
eg .1 BTC @ $10,000 BTCUSD is equal to $1,000 USD.
"""
return self.stats['current_value']
def get_pl(self):
"""
Return: <float> - a positive profit or a negative loss.
"""
return self.stats['profit']
def get_pl_pct(self):
"""
Return: <float> - a percentage - positive profit or a negative loss.
"""
return self.stats['profit_pct']
def get_status(self):
"""
Return: <str> - Returns the status of the trade.
"""
return self.status
# def place_order(self):
# """
# :return: on success: order, on fail: <str>'failed'
# """
#
# try:
# print('Trade:place_order(): Attempting to place order')
# print(f"symbol={self.symbol}, side={self.side}, type={self.order_type}, timeInForce='GTC',",
# f" quantity={self.position_size}, price={self.stats['opening_price']}")
#
# # Market orders immediately fill with best price available in the orderbook.
# if self.order_type == 'MARKET':
# order = self.exchange.client.create_test_order(symbol=self.symbol,
# side=self.side,
# type=self.order_type,
# quoteOrderQty=self.quoteOrderQty,
# newClientOrderId=self.unique_id)
# # Limit orders are posted in the orderbook.
# # The order's behavior is modified by setting time_in_force.
# # GTC (Good-Till-Cancel), IOC (Immediate-Or-Cancel), FOK (Fill-Or-Kill)
# elif self.order_type == 'LIMIT':
# order = self.exchange.client.create_test_order(symbol=self.symbol,
# side=self.side,
# type=self.order_type,
# timeInForce=self.time_in_force,
# quantity=self.position_size,
# price=self.stats['opening_price'],
# newClientOrderId=self.unique_id)
# # This is here incase I want to make a custom order type.
# else:
# print(f'Trade: No Implementation for trade.order: {self.order_type}')
# return None
# # If we get here an exception was not thrown by the exchange.
# print('Trade:place_order(): Order successfully created.')
# self.flags['order_placed'] = True
# return order
# # Report error if order fails
# except Exception as e:
# print(e, "error")
# return 'failed'
def update_values(self, current_price):
"""
Update the P/L values and percentages.
:param current_price - current price
:return: None
""" """
:param symbol: The symbol of the trading pair.
:param side: BUY|SELL
:param order_type: LIMIT|MARKET
:param position_size: Value in USD
:param price: Asking price for LIMIT orders.
"""
# Returns a datetime object containing the local date and time
self.timestamp = datetime.now()
self.symbol = symbol
self.side = side
self.order_type = order_type
self.position_size = position_size
# Flag set once order is placed successfully.
self.order_placed = False
# Order info returned when order is placed.
self.order = None
# Flag set once order is filled
self.order_filled = False
# This flag is set when the trade is closed out.
self.trade_closed = False
# Opening value of the asset.
self.opening_price = price
# Current value
self.value = 0
# Profit or Loss
self.profit_loss = 0
# Profit or Loss in percentage.
self.pl_percentage = 0
def update(self, current_price):
# Utility function.
def percent(part, whole): def percent(part, whole):
# Return a percentage.
if whole == 0: if whole == 0:
return 0 return 0
pct = 100 * float(part) / float(whole) pct = 100 * float(part) / float(whole)
return pct return pct
# Set the current value and profit/loss # Update the current price.
initial_value = self.position_size * self.opening_price self.stats['current_price'] = current_price
self.value = self.position_size * current_price
if self.side == 'buy':
self.profit_loss = self.value - initial_value
else:
self.profit_loss = initial_value - self.value
self.pl_percentage = percent(self.profit_loss, initial_value)
return self.profit_loss # Get the value at entry into the position.
initial_value = self.stats['opening_value']
# Calculate the current value in the quote currency.
self.stats['current_value'] = float(self.stats['qty_filled'] * current_price)
# Calculate and set the profit or loss.
self.stats['profit'] = self.stats['current_value'] - initial_value
# If it is a short, reverse the sign.
if self.side == 'sell':
self.stats['profit'] *= -1
# Subtract the trade in/out fees that would be paid if settled at the current price.
projected_fees = (self.stats['current_value'] * self.fee) + (self.stats['opening_value'] * self.fee)
self.stats['profit'] -= projected_fees
# Calculate and set the profit/loss percentage.
self.stats['profit_pct'] = percent(self.stats['profit'], initial_value)
return
def update(self, current_price):
"""
Update the trade and return the status of the trade.
:param current_price: <float> - Most recent price update.
:return: <str> - Trade status.
"""
# If the trade has been closed return status without updating.
if self.status == 'closed':
return self.status
# Update all price dependent values in the trade.
self.update_values(current_price)
# If everything is updated and trade is live return status.
return self.status
def order_placed(self, order):
"""
Set the status and store the order after it has been placed on the exchange.
:param order: The order received in response to placing an order on the exchange.
:return: None
"""
self.status = 'unfilled'
self.order = order
return
def trade_filled(self, qty, price):
"""
Records the quantity filled and updates the values and sets the status.
:param qty <float> - The quantity filled.
:param price <float> - The price paid.
:return: None
"""
# Todo maybe inactive flag is un-necessary.
# If the trade is being filled it must have been active.
if self.status == 'inactive':
self.status = 'unfilled'
# If none of the trade had been filled yet.
if self.status == 'unfilled':
# Record the quantity that has been filled.
self.stats['qty_filled'] = qty
# Record the price actually paid.
self.stats['opening_price'] = price
else:
# If adding to a partially filled trade, calculate and record
# the weighted average of the trade as the opening price.
sum_of_values = (qty * price) + self.stats['opening_value']
t_qty = self.stats['qty_filled'] + qty
weighted_average = sum_of_values / t_qty
self.stats['opening_price'] = weighted_average
# Add to the quantity that has been filled.
self.stats['qty_filled'] += qty
# Adjust the opening_value record.
self.stats['opening_value'] = self.stats['qty_filled'] * self.stats['opening_price']
# Set the appropriate status for the trade.
if self.stats['qty_filled'] == self.base_order_qty:
self.status = 'filled'
else:
self.status = 'part_filled'
def settle(self, qty, price):
"""
Settle all or part of the trade.
:param: qty - The quantity that was settled.
:param: price - The price the trade was settled at.
:return: None
"""
# If noting has been settled yet.
if self.stats['qty_settled'] == 0:
self.stats['settled_price'] = price
self.stats['settled_value'] = qty * price
self.stats['qty_settled'] = qty
else:
# If settling previously reduced trade, calculate and record.
# the weighted average of the settled trade as the settled price.
sum_of_values = (qty * price) + self.stats['settled_value']
t_qty = self.stats['qty_settled'] + qty
weighted_average = sum_of_values / t_qty
self.stats['settled_price'] = weighted_average
# Add to the quantity that has been settled.
self.stats['qty_settled'] += qty
# Adjust the settled value record.
self.stats['settled_value'] = self.stats['qty_settled'] * self.stats['settled_price']
# If the entire trade is settled, mark the trade as closed.
if self.stats['qty_settled'] == self.base_order_qty:
self.status = 'closed'
return
# TODO left over from changes
# Check if the trade has been filled.
# if self.flags['order_filled'] is False:
# # if it hasn't been filled cancel the order.
# resp = self.exchange.client.cancel_order(self.unique_id)
# if resp["status"] == "CANCELED":
# return True
# else:
# # Toggle the side settings.
# if self.side == 'buy':
# self.side = 'sell'
# else:
# self.side = 'buy'
# # option MARKET closed the trade for what ever it can get.
# if option == 'MARKET':
# self.order_type = 'MARKET'
# self.place_order()
class Trades: class Trades:
def __init__(self, client): def __init__(self, loaded_trades=None):
""" """
:param client: The socket connection to the exchange. This class receives, executes, tracks and stores all active_trades.
:param loaded_trades: <list?dict?> A bunch of active_trades to create and store.
""" """
# Object that maintains exchange and account data
self.exchange = None
# Socket connection to the exchange. # Socket connection to the exchange.
self.client = client self.client = None
# For automating limit orders offset the current price by 1/100 percent.
self.offset_amount = 0.0001 # Exchange fees. Will be fetched from the server and over writen on connection.
# Exchange fees. Maybe these can be fetched from the server?
self.exchange_fees = {'maker': 0.01, 'taker': 0.05} self.exchange_fees = {'maker': 0.01, 'taker': 0.05}
# Hedge mode allows long and shorts to be placed simultaneously. # Hedge mode allows long and shorts to be placed simultaneously.
self.hedge_mode = False self.hedge_mode = False
# If hedge mode is disabled this is either {'buy','sell'}. # If hedge mode is disabled this is either {'buy','sell'}.
self.side = None self.side = None
# A list of unfilled trades. # A list of active trades.
self.unfilled_trades = [] self.active_trades = []
# A list of filled trades. # A list of trades that have been closed.
self.filled_trades = [] self.settled_trades = []
# A completed trades. self.stats = {'num_trades': 0, 'total_position': 0, 'total_position_value': 0}
self.closed_trades = []
# Number of open trades.
self.num_trades = 0
# The quantity sum of all open trades.
self.total_position = 0
# The value of all open trades in USD.
self.total_position_value = 0
# Info on all futures symbols
self.info = client.futures_exchange_info()
# Dictionary of places the exchange requires after the decimal for all symbols.
self.symbols_n_precision = {}
for item in self.info['symbols']:
self.symbols_n_precision[item['symbol']] = item['quantityPrecision']
def update(self, cdata): # Load any trades that were passed into the constructor.
if loaded_trades is not None:
# Create the active_trades loaded from file.
self.load_trades(loaded_trades)
def connect_exchange(self, exchange):
"""
Connect an exchange.
:param exchange: <Exchange obj> - The exchange to connect.
:return: None
"""
self.exchange = exchange
self.client = self.exchange.client
# TODO figure out if I need anything like this.
# Find out if hedge-mode is enabled and set a local flag.
# pm = self.client.futures_get_position_mode()
# self.hedge_mode = pm.dualSidePosition
return
def exchange_connected(self):
"""
Report if an exchange has been connected to ths object.
"""
if self.exchange is None:
return False
else:
return True
def get_trades(self, form):
# Return a python object of all the trades stored in this instance.
if form == 'obj':
return self.active_trades
# Return a JSON object of all the trades stored in this instance.
elif form == 'json':
trades = self.active_trades
json_str = []
for trade in trades:
json_str.append(trade.to_json())
return json_str
# Return a dictionary object of all the trades stored in this instance.
elif form == 'dict':
trades = self.active_trades
t_list = []
for trade in trades:
dic = trade.__dict__
t_list.append(dic)
return t_list
def get_trades_by_status(self, status):
"""
Evaluate all active_trades. Return a list of active_trades with the specified
status.
:param status <str>: status to evaluate - ['inactive'|'unfilled'|'filled'|'part_filled'|'closed']
:return: List of any trade that have the flag set.
"""
ret_list = []
for trade in self.active_trades:
if trade.status is status:
ret_list.append(trade)
return ret_list
def get_trade_by_id(self, trade_id):
"""
Returns 1st trade found that matches the ID specified.
:param trade_id: unique id
:return: trade object
"""
for trade in self.active_trades:
if trade.unique_id == trade_id:
return trade
def load_trades(self, trades):
"""
Creates trade objects from a list trades and attributes.
:param trades: a list of active_trades to load.
"""
# Loop through the list of active_trades provided.
for trade in trades:
# Append a local list of active_trades with a new trade created trade object.
# Pass the properties of trade and a reference of the exchange into the constructor.
self.active_trades.append(Trade(self.exchange, **trade))
return
def place_order(self, trade):
"""
Execute a place order command on the exchange.
:param trade: Trade object
:return: on success: order, on fail: <str>'failed'
"""
try:
# Log the action to the command line.
print('\nTrades:place_order(): Attempting to place order on the exchange.')
print(f"symbol={trade.symbol}, side={trade.side}, type={trade.order_type}, timeInForce=trade.timeInForce,",
f" quantity={trade.base_order_qty}, price={trade.stats['opening_price']}")
# Market orders immediately fill with best price available in the orderbook.
if trade.order_type == 'MARKET':
order = self.exchange.client.create_test_order(symbol=trade.symbol,
side=trade.side,
type=trade.order_type,
quantity=trade.base_order_qty,
newClientOrderId=trade.unique_id)
# Limit orders are posted in the orderbook.
# The order's behavior is modified by setting time_in_force.
# GTC (Good-Till-Cancel), IOC (Immediate-Or-Cancel), FOK (Fill-Or-Kill)
elif trade.order_type == 'LIMIT':
order = self.exchange.client.create_test_order(symbol=trade.symbol,
side=trade.side,
type=trade.order_type,
timeInForce=trade.time_in_force,
quantity=trade.base_order_qty,
price=trade.stats['opening_price'],
newClientOrderId=trade.unique_id)
# This is here incase I want to make a custom order type.
else:
print(f'Trade: No Implementation for trade.order: {trade.order_type}')
return 'failed'
# If we get here an exception was not thrown by the exchange.
# Log the success to the command line.
print('Trade:place_order(): Order successfully created.')
print(order)
# Store the order and set the status flag in the trade object.
trade.order_placed(order)
return 'success'
# Report error if order fails.
except Exception as e:
# Log the error to the commandline.
print(e, "error")
return 'failed'
def update(self, price):
"""
Updates the price all active trades.
:param price: current.
:return: list of updated trade data
"""
r_update = [] r_update = []
# Check if any unfilled orders are now filled. for trade in self.active_trades:
for trade in self.unfilled_trades:
if not trade.order_filled:
order = self.client.get_order(symbol=trade.symbol, orderId=trade.order['orderId'])
if order['status'] == 'FILLED':
trade.order_filled = True
self.filled_trades.append(trade)
r_update.append({trade.timestamp: 'filled'})
# Delete filled trades from unfilled_trades
self.unfilled_trades[:] = (t for t in self.unfilled_trades if t.order_filled is False)
for trade in self.filled_trades: # If the trade hasn't been filled check the status on the exchange.
# Update the open trades. if trade.status == 'unfilled' or trade.status == 'part-filled':
ret = trade.update(cdata['close']) # Get an updated version of the order from the exchange.
r_update.append({trade.timestamp: {'pl': ret}}) posted_order = self.client.get_order(symbol=trade.symbol, orderId=trade.order.orderId)
# If a trade has been closed... # Check to see if the order is filled.
if trade.trade_closed: if posted_order.orderStatus == 'FILLED':
self.closed_trades.append(trade) # If the order is filled update the trade object.
# Notify caller trade.trade_filled(posted_order.toAmount, posted_order.fromAmount)
r_update.append({trade.timestamp: 'closed'}) else:
# Delete closed trades from filled_trades # If the order is not filled, do nothing.
self.filled_trades[:] = (t for t in self.filled_trades if t.trade_closed is False) pass
if trade.status == 'inactive':
# Check to see if this trade is supposed to be posted on the exchange.
if trade.dest == 'exchange':
# Todo: Not sure if a use-case exist where the order will fail to be placed in new_trade.
# If the trade is still inactive new_trade must have failed to place the order.
# Check to see if the order actually doesn't exist.
open_orders = self.client.get_open_orders(symbol=trade.symbol)
order_not_placed = True
for order in open_orders:
if order.orderId is trade.unique_id:
order_not_placed = False
if order_not_placed:
# TODO: Log error encountered in new_trade to avoid failing repeatedly.
# Attempt to place the order again.
result = self.place_order(trade)
if result == 'success':
print(f'Trades:Update(): Order successfully placed on exchange.')
ret = trade.update(price)
if ret == 'updated' or ret == 'trade_filled':
r_update.append({
'status': ret,
'id': trade.unique_id,
'pl': trade.stats['profit'],
'pl_pct': trade.stats['profit_pct']
})
else:
r_update.append({'id': trade.unique_id, 'status': ret})
return r_update return r_update
def new_trade(self, symbol, side, order_type, usd, price=None): def new_trade(self, target, symbol, side, order_type, qty, price=None, offset=None):
# If hedge mode is disabled return False if trade is on opposite side. """
if self.hedge_mode is False: Return a reference to a newly created a Trade object.
if self.side is None:
self.side = side
if self.side != side:
return False
# If no price is given, set the asking price to be offset by a small amount. :param target: <str> - ['exchange'|'backtester']The destination for this trade.
:param symbol - Symbol of the trading pair.
:param side - Buy|Sell
:param order_type - Market|Limit|Trailing
:param qty - Quantity of order
:param price - Price to buy at. A relative price may be set by passing a string preceded with a +/-
:param offset - Is an amount to offset a trailing limit order.
"""
# For now, you must be connected to an exchange to make a trade.
if not self.exchange_connected():
# Todo: There are some values pulled from the exchange here that make this cumbersum.
# I think this should be able to que trades and place them after connection.
# Clarify whether or not all references to precision, min and max
# values could be move to the place_order() method. To de-couple this method from Exchange.
return 'Error', 'No exchange connected.'
# The required level of precision for this trading pair.
precision = self.exchange.get_precision(symbol)
# Function to convert a value to a float set to the desired precision.
def f_val(val):
return float(f"{val:.{precision}f}")
# The minimum value aloud to be traded.
minimum_n_qty = f_val(self.exchange.get_min_notional_qty(symbol))
# The minimum quantity aloud to be traded.
minimum_qty = f_val(self.exchange.get_min_qty(symbol))
# If quantity isn't supplied set it to the smallest allowable trade.
try:
qty = f_val(qty)
except (Exception,):
qty = minimum_qty
# If quantity was supplied, but it's too low, return an error.
if qty < minimum_qty:
return 'Error', 'Order-size too low.'
# TODO: A hedge mode toggle complicates the behavior of settling and reducing trades.
# Leaving it out for now.
# Notes - A behavior diagram would be helpful to sort out the logic here.
# When do you define a side? Trading out is not a side-change.
# If hedge mode is disabled return False if trade is on opposite side.
# if self.hedge_mode is False:
# if self.side is None:
# self.side = side
# if self.side != side:
# return 'Error', 'Wrong side. Settle trade or enable hedge mode.'
# Ensure offset is at least set to the smallest allowable value of the quote asset.
if offset is None or offset < minimum_n_qty:
offset = minimum_n_qty
# If no price is given, set relative offset to the current price.
if price is None: if price is None:
offset = self.get_price(symbol) * self.offset_amount
if side == 'buy': if side == 'buy':
price = f'-{offset}' price = f'-{offset}'
else: else:
price = f'+{offset}' price = f'+{offset}'
# A relative limit order may be set by passing a string preceded with a +/- # If price is a string process and convert it to a float.
if type(price) == str: if type(price) == str:
# If it is proceeded with a +/-, processes as a relative limit order.
if ('+' in price) or ('-' in price): if ('+' in price) or ('-' in price):
price = self.get_price(symbol) + float(price) # Covert the relative value into an absolute offset of the current price.
else: price = self.exchange.get_price(symbol) + float(price)
price = float(price) # Convert the string into a float.
price = f_val(price)
position_size = usd / price # position_size is the value of the trade in the parent currency.
# The required level of precision for this trading pair. # eg .1 BTC @ $10,000 USD/BTC is equal to $1,000 USD.
precision = self.symbols_n_precision[symbol] position_size = f_val(qty * price)
# String representing the order amount formatted to the level of precision defined above.
order_amount = "{:0.0{}f}".format(position_size, precision)
# Create a trade and place the order.
trade = Trade(symbol, side, order_type, order_amount, price)
order = self.place_order(trade)
# If the order successfully placed store the trade.
if order:
# Set th order placed flag.
trade.order_placed = True
# Save the order info in the trade object.
trade.order = order
# Update the trades instance. # Log to terminal for development. Todo remove when stable.
self.num_trades += 1 print('\nTrades:new_trade(): Creating a trade object.')
self.unfilled_trades.append(trade) print(f'Order amount: {position_size:.6f}')
print(order) print(f'Minimum usd: {minimum_n_qty:.2f}')
return trade
# Create a trade.
trade = Trade(dest=target, symbol=symbol, side=side, price=price, qty=qty, order_type=order_type, tif='GTC',
unique_id=None, status=None, stats=None, order=None)
# If the trade is meant to be on the exchange. Call the method to place the trade on the exchange.
if target == 'exchange':
# Attempt to place the order.
result = self.place_order(trade)
if result == 'success':
print(f'Trades:new_trade(): Order successfully placed on exchange.')
# Add the trade to a list of active_trades instance.
self.active_trades.append(trade)
# Return the trade ID
return 'Success', trade.unique_id
def close_trade(self, trade_id):
"""
Close the trade.
:param trade_id: <hex> - The unique ID of the trade.
:return: <bool> - Return True if successful.
"""
trade = self.get_trade_by_id(trade_id)
qty = trade.base_order_qty
price = self.exchange.get_price(trade.symbol)
trade.settle(qty=qty, price=price)
if trade.status == 'closed':
self.active_trades.remove(trade)
self.settled_trades.append(trade)
return True
else: else:
return False return False
@staticmethod def reduce_trade(self, trade_id, qty):
def get_price(symbol): """
# Todo: Make sure im getting the correct market price for futures. Reduce the position of a trade.
request = requests.get(f'https://api.binance.com/api/v3/ticker/price?symbol={symbol}')
json_obj = json.loads(request.text)
return json_obj['price']
def place_order(self, trade): :param trade_id: <hex> - The Unique id of a tradee
if trade.order_type == 'MARKET': :param qty: <float> - The quantity to reduce the trade by.
try: :return: <float> - The base quantity left over.
order = self.client.create_test_order(symbol=trade.symbol, """
side=trade.side, # Fetch the trade object.
type=trade.order_type, trade = self.get_trade_by_id(trade_id)
quantity=trade.position_size) if trade.status == 'closed':
print('!!!Order created!!!') # Can't reduce a trade that's been closed.
return order
# Report error if order fails
except Exception as e:
print(e, "error")
return None
elif trade.order_type == 'LIMIT':
try:
order = self.client.create_test_order(
symbol=trade.symbol, side=trade.side, type=trade.order_type,
timeInForce='GTC', quantity=trade.position_size,
price=trade.opening_price)
return order
# If order fails
except Exception as e:
print(e, "error")
return None
else:
print(f'Trade: No Implementation for trade.order: {trade.order_type}')
return None return None
if trade.status == 'inactive' or trade.status == 'unfilled':
# If it has been placed but not filled.
if trade.status == 'unfilled':
# Cancel the order.
self.client.cancel_order(symbol=trade.symbol, orderId=trade.unique_id)
# The order hasn't been placed so just reduce the order quantity.
# Make sure you are not reducing more than the original order.
if qty > trade.base_order_qty:
qty = trade.base_order_qty
trade.base_order_qty -= qty
# Adjust the opening value.
trade.stats['opening_value'] = (trade.base_order_qty * trade.stats['opening_price'])
# Re-calculate values so the PL will represent the new value - fees.
trade.update_values(trade.stats['opening_price'])
# If it has been previously placed but not filled and now canceled.
if trade.status == 'unfilled':
# Replace the order.
self.place_order(trade=trade)
# Return the new total quantity to buy.
return trade.base_order_qty
# You can't settle more than you own.
if qty > trade.stats['qty_filled']:
qty = trade.stats['qty_filled']
# Retrieve the current price.
price = self.exchange.get_price(trade.symbol)
# Settle the quantity requested.
trade.settle(qty=qty, price=price)
# If the trade is closed remove from the list of open active_trades.
if trade.status == 'closed':
self.active_trades.remove(trade)
self.settled_trades.append(trade)
return 0
else:
# If the trade is not closed then return the total left open.
left = trade.stats['qty_filled'] - trade.stats['qty_settled']
return float(f"{left:.3f}")