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="modifyBaseFiles" value="true" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="py.test" />
</component>
</module>

View File

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

View File

@ -1,3 +1,4 @@
import json
from dataclasses import dataclass
@ -51,19 +52,14 @@ class Signal:
class Signals:
def __init__(self):
self.signals = []
# self.set_signals_defaults()
def __init__(self, loaded_signals=None):
def set_signals_defaults(self):
"""These defaults are loaded if the config file is not found."""
sigs = self.get_signals_defaults()
for sig in sigs:
self.signals.append(Signal(name=sig['name'], source1=sig['source1'],
prop1=sig['prop1'], operator=sig['operator'],
source2=sig['source2'], prop2=sig['prop2'],
state=sig['state']))
return
# list of Signal objects.
self.signals = []
# Initialize signals with loaded data.
if loaded_signals is not None:
self.create_signal_from_dic(loaded_signals)
@staticmethod
def get_signals_defaults():
@ -85,15 +81,41 @@ class Signals:
"value2": None, "range": None}
return [s1, s2, s3]
def init_loaded_signals(self, signals_list):
for sig in signals_list:
self.signals.append(Signal(name=sig['name'], source1=sig['source1'],
prop1=sig['prop1'], operator=sig['operator'],
source2=sig['source2'], prop2=sig['prop2'],
state=sig['state']))
def create_signal_from_dic(self, signals_list=None):
"""
:param signals_list: list of dict
:return True: on success.
Create and store signal objects from list of dictionaries.
"""
def get_signals(self):
return self.signals
# If no signals were provided used a default list.
if signals_list is None:
signals_list = self.get_signals_defaults()
# Loop through the provided list, unpack the dictionaries, create and store the signal objects.
for sig in signals_list:
self.new_signal(sig)
return True
def get_signals(self, form):
# Return a python object of all the signals stored in this instance.
if form == 'obj':
return self.signals
# Return a JSON object of all the signals stored in this instance.
elif form == 'json':
sigs = self.signals
json_str = []
for sig in sigs:
# TODO: note - Explore why I had to treat signals and strategies different here.
json_str.append(json.dumps(sig.__dict__))
return json_str
# Return a dictionary object of all the signals stored in this instance.
elif form == 'dict':
sigs = self.signals
s_list = []
for sig in sigs:
dic = sig.__dict__
s_list.append(dic)
return s_list
def get_signal_by_name(self, name):
for signal in self.signals:

View File

@ -1,25 +1,200 @@
import json
class Strategy:
def __init__(self, **args):
"""
:param args: An object containing key_value pairs representing strategy attributes.
Strategy format is defined in strategies.js
"""
self.current_value = None
self.opening_value = None
self.gross_pl = None
self.net_pl = None
self.combined_position = None
# A strategy is defined in Strategies.js it is received from the client,
# then unpacked and converted into a python object here.
for name, value in args.items():
# Make each keyword-argument a property of the class.
setattr(self, name, value)
# A container to hold previous state of signals.
self.last_states = {}
# A list of all the trades made by this strategy.
self.trades = []
def get_position(self):
return self.combined_position
def get_pl(self):
self.update_pl()
return self.net_pl
def update_pl(self):
# sum the pl of all the trades.
position_sum = 0
pl_sum = 0
opening_value_sum = 0
value_sum = 0
for trade in self.trades:
pl_sum += trade.profit_loss
position_sum += trade.position_size
value_sum += trade.value
opening_value_sum += trade.opening_value
self.combined_position = position_sum
self.gross_pl = pl_sum
self.opening_value = opening_value_sum
self.current_value = value_sum
def to_json(self):
return json.dumps(self, default=lambda o: o.__dict__,
sort_keys=True, indent=4)
def evaluate_strategy(self, signals):
"""
:param signals: Signals: A reference to an object that handles current signal states.
:return action: Action required based on evaluation. format{cmd:str, amount:real, margin:int}
"""
def condition_satisfied(sig_name, value):
"""
Check if a signal has a state of value.
:param sig_name: str: The name of a signal object to compare states.
:param value: The state value to compare.
:return bool: True: <Signal: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:
def __init__(self, loaded_strats, trades):
self.strat_list = loaded_strats
# Reference to the trades object that maintains all trading actions and data.
self.trades = trades
# A list of all the Strategies created.
self.strat_list = []
# Initialise all the stately objects with the data saved to file.
for entry in loaded_strats:
self.strat_list.append(Strategy(**entry))
def new_strategy(self, data):
self.strat_list.append(data)
# Create an instance of the new Strategy.
self.strat_list.append(Strategy(**data))
def delete_strategy(self, name):
obj = self.get_strategy_by_name(name)
if obj:
self.strat_list.remove(obj)
def get_strategies(self):
return self.strat_list
def get_strategies(self, form):
# Return a python object of all the strategies stored in this instance.
if form == 'obj':
return self.strat_list
# Return a JSON object of all the strategies stored in this instance.
elif form == 'json':
strats = self.strat_list
json_str = []
for strat in strats:
json_str.append(strat.to_json())
return json_str
# Return a dictionary object of all the strategies stored in this instance.
elif form == 'dict':
strats = self.strat_list
s_list = []
for st in strats:
dic = st.__dict__
s_list.append(dic)
return s_list
def get_strategy_by_name(self, name):
for obj in self.strat_list:
if obj['name'] == name:
if obj.name == name:
return obj
return False
@ -27,33 +202,34 @@ class Strategies:
order_type = 'LIMIT'
if action == 'open_position':
# Attempt to create the trade.
trade_id = self.trades.new_trade(strategy['symbol'], cmd, order_type, strategy['trade_amount'])
# If the trade didn't fail.
if trade_id is not False:
# Set the active flag in strategy.
strategy['active'] = True
strategy['current_position'] += strategy['trade_amount']
strategy['trades'].append(trade_id)
return 'position_opened'
else:
print('Failed to place trade')
status, result = self.trades.new_trade(strategy.symbol, cmd, order_type, strategy.trade_amount)
# If the trade failed.
if status == 'Error':
print(status, result)
return 'failed'
else:
# Set the active flag in strategy.
strategy.active = True
strategy.current_position += strategy.trade_amount
strategy.trades.append(result)
return 'position_opened'
if (action == 'stop_loss') or (action == 'take_profit'):
if action == 'stop_loss':
order_type = 'MARKET'
# Attempt to create the trade.
trade = self.trades.new_trade(strategy['symbol'], cmd, order_type, strategy['current_position'])
# If the trade didn't fail.
if trade is not False:
status, result = self.trades.new_trade(strategy['symbol'], cmd, order_type, strategy['current_position'])
# If the trade failed.
if status == 'Error':
print(status, result)
return 'failed'
else:
# Set the active flag in strategy.
strategy['active'] = False
strategy['current_position'] = 0
return 'position_closed'
else:
print('Failed to place trade')
return 'failed'
print('Strategies.execute_cmd: Invalid action received.')
print(f'Strategies.execute_cmd: Invalid action received: {action}')
return 'failed'
def update(self, signals):
@ -62,99 +238,28 @@ class Strategies:
published strategies and evaluates conditions against the data.
This function returns a list of strategies and action commands.
"""
# Object containing data to return to function caller.
actions = {}
def process_strategy(strategy):
action, cmd = strategy.evaluate_strategy(signals)
if action != 'do_nothing':
# Execute the command.
return {'action': action, 'result': self.execute_cmd(strategy, action, cmd)}
return {'action': 'none'}
def get_stats(strategy):
position = strategy.get_position()
pl = strategy.get_pl()
stats = {'pos': position, 'pl': pl}
return stats
# Data object returned to function caller.
return_obj = {}
# Loop through all the published strategies.
for strategy in self.strat_list:
# Process any take_profit strategy.
if strategy['type'] == 'take_profit':
action, cmd = self.eval_tp_stg(strategy, signals)
if action == 'do_nothing':
return False
else:
# Execute the command.
actions[strategy['name']] = self.execute_cmd(strategy, action, cmd)
else:
print(f"Strategy.update: Strategy of type {strategy['type']} - not yet implemented.")
if len(actions) != 0:
return actions
else:
actions = process_strategy(strategy)
stat_updates = get_stats(strategy)
return_obj[strategy.name] = {'actions': actions, 'stats': stat_updates}
if len(return_obj) == 0:
return False
def eval_tp_stg(self, strategy, signals):
"""
:param strategy: str: The strategy to evaluate.
:param signals: Signals: A reference to an object that handles current signal states.
:return action: Action required based on evaluation. format{cmd:str, amount:real, margin:int}
"""
def condition_satisfied(sig_name, vlue):
signal = signals.get_signal_by_name(sig_name)
if vlue == json.dumps(signal.state):
return True
else:
return False
def all_conditions_met(conditions):
if len(conditions) < 1:
print(f"no trade-in conditions supplied: {strategy['name']}")
return False
# Evaluate all conditions and return false if any are un-met.
for trigger_signal in conditions.keys():
trigger_value = conditions[trigger_signal]
# Compare this signal's state with the trigger_value
if not condition_satisfied(trigger_signal, trigger_value):
return False
return True
def trade_out_condition_met(condition_type):
if strategy[condition_type]['typ'] == 'conditional':
signal_name = strategy[condition_type]['trig']
signal_value = strategy[condition_type]['val']
if condition_satisfied(signal_name, signal_value):
# If the condition is met trade-out.
return True
else:
return False
else:
if strategy[condition_type]['typ'] != 'value':
raise ValueError('trade_out_condition_met: invalid condition_type')
if condition_type == 'take_profit':
# If the profit condition is met send command to take profit.
if strategy['gross_profit'] > strategy['take_profit']['val']:
return True
else:
return False
else:
# If the loss condition is met, return a trade-out command.
if strategy['gross_loss'] < strategy['stop_loss']['val']:
return True
else:
return False
trade_in_cmd = strategy['side']
if strategy['side'] == 'buy':
trade_out_cmd = 'sell'
else:
trade_out_cmd = 'buy'
return return_obj
# If trade-in conditions are met.
if all_conditions_met(strategy['trd_in_conds']):
# If the new trade wouldn't exceed max_position. Return a trade-in command.
proposed_position_size = strategy['current_position'] + strategy['trade_amount']
if proposed_position_size < strategy['max_position']:
return 'enter_position', trade_in_cmd
# If strategy is active test the take-profit or stop-loss conditions.
if strategy['active']:
# Conditional take-profit trades-out if a signals equals a set value.
if trade_out_condition_met('take_profit'):
return 'take_profit', trade_out_cmd
# Conditional stop-loss trades-outs if a signals value equals a set value.
if trade_out_condition_met('stop_loss'):
return 'stop_loss', trade_out_cmd
# No conditions were met.
print('Strategies were updated and nothing to do.')
return 'do_nothing', 'nothing'

279
app.py
View File

@ -1,28 +1,33 @@
import json
# Flask is a lightweight html server used to render the UI.
from flask import Flask, render_template, request, redirect, jsonify
from flask_cors import cross_origin
from binance.enums import *
from flask_sock import Sock
# Handles all server side data and interactions.
# Handles all updates and requests for locally stored data.
from data import BrighterData
# Define app
app = Flask(__name__)
sock = Sock(app)
# Create a Flask object named interface that serves the html.
interface = Flask(__name__)
# Create a socket in order to receive requests.
sock = Sock(interface)
# This object maintains all the application and historical data.
# Access to server, local storage, other classes go through here.
# Create a BrighterData object. This the main application that maintains access to the server, local storage,
# and manages objects that process trade data.
app_data = BrighterData()
# app.config['SECRET_KEY'] = 'The quick brown fox jumps over the lazy dog'
# app.config['CORS_HEADERS'] = 'Content-Type'
# cors = CORS(app, resources={r"*": {"origins": "*"}})
@app.route('/')
# TODO: The cors object had something to do with an error I was getting while
# receiving json. It may not be needed anymore.
# interface.config['CORS_HEADERS'] = 'Content-Type'
# cors = CORS(interface, resources={r"*": {"origins": "*"}})
@interface.route('/')
def index():
# Passes data into an HTML template and serves it to a locally hosted server
# Generate and return a landing page for the web application.
# Fetch data from app_data and inject it into an HTML template.
# Render the template and serve it upon request.
rendered_data = app_data.get_rendered_data()
js_data = app_data.get_js_init_data()
return render_template('index.html',
@ -40,77 +45,111 @@ def index():
@sock.route('/ws')
def ws(sock):
# TODO: Note I started using a socket to receive messages from the UI rather then the
# Flask interfaces because it was getting complicated to return data to the UI objects.
# I was getting error messages regarding headers while transferring json objects
# and found it problematic dealing with browser refreshes.
# Define a websocket to handle request originating from the UI.
# Handle incoming json msg's.
def json_msg_received(msg_obj):
if 'message_type' in msg_obj:
if msg_obj['message_type'] == 'candle_data':
# Send the candle to the BrighterData_obj
# and forward any returned data to the client.
r_data = app_data.received_cdata(msg_obj['data'])
if r_data:
resp = {
"reply": "updates",
"data": r_data
}
sock.send(json.dumps(resp))
if msg_obj['message_type'] == 'request':
if msg_obj['data'] == 'signals':
signals = app_data.get_signals()
if signals:
resp = {
"reply": "signals",
"data": signals
}
resp = json.dumps(resp)
sock.send(resp)
elif msg_obj['data'] == 'strategies':
strategies = app_data.get_strategies()
if strategies:
resp = {
"reply": "strategies",
"data": strategies
}
resp = json.dumps(resp)
sock.send(resp)
else:
print('Warning: Unhandled request!')
print(msg_obj['data'])
if msg_obj['message_type'] == 'delete_signal':
app_data.delete_signal(msg_obj['data'])
if msg_obj['message_type'] == 'delete_strategy':
app_data.delete_strategy(msg_obj['data'])
if msg_obj['message_type'] == 'reply':
print(msg_obj['rep'])
print('Reply')
if msg_obj['message_type'] == 'new_signal':
# Send the data to the BrighterData_obj
# and forward any returned data to the client.
r_data = app_data.received_new_signal(msg_obj['data'])
if r_data:
resp = {
"reply": "signal_created",
"data": r_data
}
resp = json.dumps(resp)
sock.send(resp)
if msg_obj['message_type'] == 'new_strategy':
# Send the data to the BrighterData_obj
# and forward any returned data to the client.
r_data = app_data.received_new_strategy(msg_obj['data'])
if r_data:
resp = {
"reply": "strategy_created",
"data": r_data
}
resp = json.dumps(resp)
sock.send(resp)
if 'message_type' not in msg_obj:
return
if msg_obj['message_type'] == 'candle_data':
# If we received candle data. Send it to the BrighterData_obj
# and forward any returned data to the client.
r_data = app_data.received_cdata(msg_obj['data'])
if r_data:
resp = {
"reply": "updates",
"data": r_data
}
sock.send(json.dumps(resp))
if msg_obj['message_type'] == 'request':
# If a request for data is received fetch it from the appropriate object
# and return.
if msg_obj['data'] == 'signals':
signals = app_data.get_signals()
if signals:
resp = {
"reply": "signals",
"data": signals
}
resp = json.dumps(resp)
sock.send(resp)
elif msg_obj['data'] == 'strategies':
strategies = app_data.get_strategies()
if strategies:
resp = {
"reply": "strategies",
"data": strategies
}
resp = json.dumps(resp)
sock.send(resp)
elif msg_obj['data'] == 'trades':
trades = app_data.get_trades()
if trades:
resp = {
"reply": "trades",
"data": trades
}
resp = json.dumps(resp)
sock.send(resp)
else:
print('Warning: Unhandled request!')
print(msg_obj['data'])
# If the message is a command.
# Pass the command and data on to app_data to process.
if msg_obj['message_type'] == 'delete_signal':
app_data.delete_signal(msg_obj['data'])
if msg_obj['message_type'] == 'delete_strategy':
app_data.delete_strategy(msg_obj['data'])
if msg_obj['message_type'] == 'new_signal':
# Send the data to the BrighterData object.
# and forward any returned data to the client.
r_data = app_data.received_new_signal(msg_obj['data'])
if r_data:
resp = {
"reply": "signal_created",
"data": r_data
}
resp = json.dumps(resp)
sock.send(resp)
if msg_obj['message_type'] == 'new_strategy':
# Send the data to the BrighterData_obj
# and forward any returned data to the client.
r_data = app_data.received_new_strategy(msg_obj['data'])
if r_data:
resp = {
"reply": "strategy_created",
"data": r_data
}
resp = json.dumps(resp)
sock.send(resp)
if msg_obj['message_type'] == 'new_trade':
# Send the data to the BrighterData_obj
# and forward any returned data to the client.
r_data = app_data.received_new_trade(msg_obj['data'])
if r_data:
resp = {
"reply": "trade_created",
"data": r_data
}
resp = json.dumps(resp)
sock.send(resp)
# If the message is a reply log the response to the terminal.
if msg_obj['message_type'] == 'reply':
print(f"\napp.py:Received reply: {msg_obj['rep']}")
return
# The rendered page connects to the exchange and relays the candle data back here
# this socket also handles data and processing requests
while True:
@ -125,38 +164,62 @@ def ws(sock):
print(f'Msg received from client: {msg}')
@app.route('/buy', methods=['POST'])
@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
def buy():
print('This buy route is currently not being used.')
# app_data.trades.new_trade(
# symbol=request.form['symbol'], side=SIDE_BUY,
# type=ORDER_TYPE_MARKET, quantity=request.form['quantity'])
return redirect('/')
# @interface.route('/trade', methods=['POST'])
# @cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
# def trade():
# # todo: This interface method is a straight forward approach to communicating from the frontend to backend
# # it only require an html form on the UI side. It's difficult to return a response without refreshing the UI.
# # I may be missing a solution that doesn't require rendering a hidden frame in the UI and complicating the code
# # further. I am abandoning this approach for socket method above. Although I am uncomfortable with the loop
# # the above solution requires. The server probably has already implemented such a loop so it seems inefficient.
# # Also I would like to implement data processing loops on the server side that may compete for resources.
# # One solution is to create another interface /trade_response. This would require a buffer so nothing gets
# # missed or overwritten. /trade_response would be polled by the UI at set intervals. Not sure if it
# # would decrease overhead to the server. I'm keeping this here for future consideration.
# def fetch(attr):
# # Verify and validate input data.
# if attr in request.form and request.form[attr] != '':
# return request.form[attr]
# else:
# return None
# # Forward the request to data.
# status, result = app_data.received_new_trade(symbol=fetch('symbol'), price=fetch('price'),
# side=fetch('side'), order_type=fetch('orderType'),
# quantity=fetch('quantity'))
# # Log any error to the terminal.
# if status == 'Error':
# print(f'\napp.py:trade() - Error placing the trade: {result}')
# # Log order to terminal.
# print(f"\napp.py:trade() - Trade order received: symbol={fetch('symbol')},"
# f" side={fetch('side')}, type={fetch('orderType')}, quantity={fetch('quantity')},price={fetch('price')}")
#
# return redirect('/')
@app.route('/sell', methods=['POST'])
@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
def sell():
print('This sell route is currently not being used.')
# app_data.trades.new_trade(
# symbol=request.form['symbol'], side=SIDE_SELL,
# type=ORDER_TYPE_MARKET, quantity=request.form['quantity'])
return redirect('/')
@app.route('/settings', methods=['POST'])
@interface.route('/settings', methods=['POST'])
@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
def settings():
"""
This route is used to change setting of the trading app
that require a browser refresh. Options are:
interval: For setting the time frame the app is trading in.
trading_pair: The pair being traded.
toggle_indicator: enables or disables indicators
edit_indicator: edits the properties of specific indicators.
new_indicator: Creates a new indicator and stores it.
:return: redirects the browser to the index /
"""
setting = request.form['setting']
if setting == 'interval':
interval_state = request.form['timeframe']
app_data.config.chart_interval = interval_state
app_data.config.set_chart_interval(interval_state)
elif setting == 'trading_pair':
trading_pair = request.form['trading_pair']
app_data.config.trading_pair = trading_pair
trading_pair = request.form['symbol']
app_data.config.set_trading_pair(trading_pair)
elif setting == 'toggle_indicator':
# Get a list of indicators to enable
# Create a list of all the enabled indicators.
enabled_indicators = []
for i in request.form:
if request.form[i] == 'indicator':
@ -224,23 +287,27 @@ def settings():
return redirect('/')
@app.route('/history')
@interface.route('/history')
@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
def history():
# Returns the candle history of a specific trading pair and interval.
# Currently, set to last 1000 candles.
symbol = app_data.config.trading_pair
interval = app_data.config.chart_interval
return jsonify(app_data.candles.get_candle_history(symbol, interval, 1000))
@app.route('/saved_data')
@interface.route('/saved_data')
@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
def saved_data():
# Returns the saved settings for the indicators
return jsonify(app_data.indicators.indicator_list)
@app.route('/indicator_init')
@interface.route('/indicator_init')
@cross_origin(origin='localhost', headers=['Content- Type', 'Authorization'])
def indicator_init():
# Initializes the indicators and returns the data for a given symbol and interval.
symbol = app_data.config.trading_pair
interval = app_data.config.chart_interval
d = app_data.indicators.get_indicator_data(symbol, interval, 800)

View File

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

176
data.py
View File

@ -1,10 +1,7 @@
from binance.client import Client
import config
from Strategies import Strategies
from candles import Candles
from Configuration import Configuration
from exchange_info import ExchangeInfo
from exchange import Exchange
from indicators import Indicators
from Signals import Signals
from trade import Trades
@ -14,39 +11,34 @@ import json
class BrighterData:
def __init__(self):
# Initialise a connection to the Binance client API
self.client = Client(config.API_KEY, config.API_SECRET)
# Object that maintains signals
self.signals = Signals()
# Object that interacts and maintains exchange and account data
self.exchange = Exchange()
# Configuration and settings for the user interface and charts
self.config = Configuration()
# Load any saved data from file
self.config.config_and_states('load')
# Initialize signals with loaded data.
self.signals.init_loaded_signals(self.config.signals_list)
# Object that maintains signals. Initialize with any signals loaded from file.
self.signals = Signals(self.config.signals_list)
# Object that maintains candlestick and price data.
self.candles = Candles(self.config, self.client)
self.candles = Candles(self.config, self.exchange)
# Object that interacts with and maintains data from available indicators
self.indicators = Indicators(self.candles, self.config)
# Object that maintains exchange and account data
self.exchange_info = ExchangeInfo(self.client)
# Object that maintains the strategies data
self.trades = Trades(self.client)
# Object that maintains the trades data
self.trades = Trades(self.config.trades)
# The Trades object needs to connect to an exchange.
self.trades.connect_exchange(exchange=self.exchange)
# Object that maintains the strategies data
self.strategies = Strategies(self.config.strategies_list, self.trades)
def get_js_init_data(self):
"""Returns a JSON object of initialization data
for the javascript in the rendered HTML"""
"""
Returns a JSON object of initialization data.
This is passed into the frontend HTML template for the javascript to access in the rendered HTML.
"""
js_data = {'i_types': self.indicators.indicator_types,
'indicators': self.indicators.indicator_list,
'interval': self.config.chart_interval,
@ -55,13 +47,13 @@ class BrighterData:
def get_rendered_data(self):
"""
Data to be rendered in the HTML
Data to be injected into the HTML template that renders the frontend UI.
"""
rd = {}
rd['title'] = self.config.application_title # Title of the page
rd['my_balances'] = self.exchange_info.balances # Balances on the exchange
rd['symbols'] = self.exchange_info.symbols # Symbols information from the exchange
rd['intervals'] = self.exchange_info.intervals # Time candle time intervals available to stream
rd['my_balances'] = self.exchange.balances # Balances on the exchange
rd['symbols'] = self.exchange.symbols # Symbols information from the exchange
rd['intervals'] = self.exchange.intervals # Time candle time intervals available to stream
rd['chart_interval'] = self.config.chart_interval # The charts current interval setting
rd['indicator_types'] = self.indicators.indicator_types # All the types indicators Available
rd['indicator_list'] = self.indicators.get_indicator_list() # indicators available
@ -70,83 +62,96 @@ class BrighterData:
return rd
def received_cdata(self, cdata):
# If this is the first candle received,
# then set last_candle. Then set a new candle.
"""
This is called to pass in new price data when it is received.
:param cdata: <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:
# Set last_candle.
self.candles.last_candle = cdata
# If this candle is the same as last candle return nothing to do.
elif cdata['time']:
if cdata['time'] == self.candles.last_candle['time']:
return
# If this is not the first candle, but it's the same as the last candle recorded.
elif cdata['time'] == self.candles.last_candle['time']:
# Return without doing anything.
return None
# New candle is received update the instance data records. And the indicators.
# A new candle is received with a different timestamp from the last candle.
# Update the instance data records.
self.candles.set_new_candle(cdata)
# Update the indicators and receive a dictionary of indicator results.
i_updates = self.indicators.update_indicators()
# Process the signals based on the last indicator updates.
# Now that all the indicators have changed. Process the signals and receive a list of signals that
# have changed their states.
state_changes = self.signals.process_all_signals(self.indicators)
# Update the trades instance.
# Update the trades instance with the new price data.
trade_updates = self.trades.update(cdata)
# Update the strategies instance.
# Update the strategies instance. Strategy execution is based on the signal states and trade values.
# This must be updated last.
stg_updates = self.strategies.update(self.signals)
# Format and return an update object.
updates = {'i_updates': i_updates}
# Format and return an update object to pass information to the frontend UI.
updates = {}
if i_updates:
updates.update({'i_updates': i_updates})
if state_changes:
print(state_changes)
updates.update({'s_updates': state_changes})
if stg_updates:
print(stg_updates)
updates.update({'stg_updts': stg_updates})
if trade_updates:
print(trade_updates)
updates.update({'trade_updts': trade_updates})
if stg_updates:
print(stg_updates)
updates.update({'stg_updts': stg_updates})
return updates
def received_new_signal(self, data):
# Check the data.
"""
This is called when a new Signal has been defined and created in the UI.
:param data: <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:
return 'data.py:received_new_signal() - The new signal has no name. '
# Forward the new signal data to the signals instance. So it can create a new signal.
self.signals.new_signal(data)
# Forward the new signal data to config. So it can save it to file.
self.config.new_signal(data)
# Update config's list of signals and save it to file.
self.config.update_data('signals', self.signals.get_signals('dict'))
# Send the data back to where it came from.
return data
def received_new_strategy(self, data):
# Check the data.
"""
This is called when a new Strategy has been defined and created in the UI.
:param data: <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:
return 'data.py:received_new_strategy() - The new strategy has no name. '
# Forward the new strategy data to the strategy's instance. So it can create a new strategy.
self.strategies.new_strategy(data)
# Forward the new signal data to config. So it can save it to file.
self.config.new_strategy(data)
# Update config's list of strategies and save to file.
self.config.update_data('strategies', self.strategies.get_strategies('dict'))
# Send the data back to where it came from.
return data
def delete_strategy(self, strategy_name):
# Delete the signal from the signals instance.
# Delete the strategy from the strategies instance.
self.strategies.delete_strategy(strategy_name)
# Delete the signal from the configuration file.
# Delete the strategy from the configuration file.
self.config.remove('strategies', strategy_name)
def get_signals(self):
""" Return a JSON object of all the signals in the signals instance."""
sigs = self.signals.get_signals()
json_str = []
for sig in sigs:
json_str.append(json.dumps(sig.__dict__))
return json_str
"""Return a JSON object of all the signals in the signals instance."""
return self.signals.get_signals('json')
def get_strategies(self):
""" Return a JSON object of all the signals in the signals instance."""
strats = self.strategies.get_strategies()
json_str = []
for strat in strats:
json_str.append(json.dumps(strat))
return json_str
""" Return a JSON object of all the strategies in the strategies instance."""
return self.strategies.get_strategies('json')
def delete_signal(self, signal_name):
# Delete the signal from the signals instance.
@ -154,3 +159,48 @@ class BrighterData:
# Delete the signal from the configuration file.
self.config.remove('signals', signal_name)
def received_new_trade(self, data):
"""
This is called when a new trade has been defined and created in the UI.
Todo: Note - I handled this differently then signals and strategies. Is this better or not?
:param data: <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)
return enabled_indicators
def get_indicator_data(self, symbol=None, interval=None, num_results=800):
def get_indicator_data(self, symbol=None, interval=None, start_ts=None, num_results=800):
# Loop through all the indicators. If enabled, run the appropriate
# update function. Return all the results as a dictionary object.
if symbol is not None:
print(symbol)
print('get_indicator_data() no symbol implementation')
if self.candles.config.trading_pair != symbol:
print(f'get_indicator_data: request candle data for {symbol}')
if interval is not None:
print(interval)
print('get_indicator_data() no interval implementation')
if self.candles.config.chart_interval != interval:
print(f'get_indicator_data: request candle data for {interval}')
if start_ts is not None:
print(f'get_indicator_data: request candle data from: {start_ts}')
# Get a list of indicator objects and a list of enabled indicators names.
i_list = self.indicators
enabled_i = self.get_enabled_indicators()
candles = self.candles
result = {}
# Loop through all indicator objects in i_list
for each_i in i_list:
# If the indicator's not enabled skip to next each_i
if each_i not in enabled_i:
continue
result[each_i] = i_list[each_i].calculate(self.candles, num_results)
result[each_i] = i_list[each_i].calculate(candles, num_results)
return result
def delete_indicator(self, indicator):

View File

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

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

View File

@ -1,27 +1,54 @@
class Comms {
constructor(ocu, occ, oiu) {
// Register callbacks
this.on_candle_update = ocu;
this.on_candle_close = occ;
this.on_indctr_update = oiu;
constructor() {
// List of callbacks callback function that will receive various updates.
this.on_candle_update = [];
this.on_candle_close = [];
this.on_indctr_update = [];
// Status of connection.
this.connection_open = false;
}
register_callback(target, callback_func){
// Register any outside functions that need to receive updates.
if (target=='candle_update'){
this.on_candle_update.push(callback_func)
}
else if (target=='candle_close'){
this.on_candle_close.push(callback_func)
}
else if (target=='indicator_update'){
this.on_indctr_update.push(callback_func)
}
else{console.log('Comms: Invalid target for callback');}
}
candle_update(new_candle){
// Call the callback provided.
this.on_candle_update(new_candle);
// If no callback is registered do nothing.
if (this.on_candle_update == null) {return};
// Call each registered callback passing the candle updates.
for (i = 0; i < this.on_candle_update.length; i++) {
this.on_candle_update[i](new_candle);
}
}
candle_close(new_candle){
// Forward a copy of the new candle to the server.
this.app_con.send( JSON.stringify({ message_type: "candle_data", data :new_candle }));
// Call the callback provided.
this.on_candle_close(new_candle);
// If no callback is registered do return.
if (this.on_candle_close == null) {return};
// Call each registered callback passing the candle updates.
for (i = 0; i < this.on_candle_close.length; i++) {
this.on_candle_close[i](new_candle);
}
}
indicator_update(data){
// Call the callback provided.
this.on_indctr_update(data);
// If no callback is registered do return.
if (this.on_indctr_update == null) {return};
// Call each registered callback passing the candle updates.
for (i = 0; i < this.on_indctr_update.length; i++) {
this.on_indctr_update[i](data);
}
}
getPriceHistory(){
@ -113,8 +140,9 @@ class Comms {
})
}
set_exchange_con(interval){
let ws = "wss://stream.binance.com:9443/ws/btcusdt@kline_" + interval;
set_exchange_con(interval, tradingPair){
tradingPair = tradingPair.toLowerCase();
let ws = "wss://stream.binance.com:9443/ws/" + tradingPair + "@kline_" + interval;
this.exchange_con = new WebSocket(ws);
// Set the on-message call-back for the socket

15
static/controls.js vendored
View File

@ -26,5 +26,20 @@ class Controls {
el.style.top = y + "px";
el.style.display = "block";
}
init_TP_selector(){
var demoInput = document.getElementById('symbol');
demoInput.value = window.UI.data.trading_pair; // set default value instead of html attribute
demoInput.onfocus = function() { demoInput.value =''; }; // on focus - clear input
demoInput.onblur = function() { demoInput.value = window.UI.data.trading_pair; }; // on leave restore it.
demoInput.onchange = function() {
var options = document.getElementById('symbols').getElementsByTagName('option');
this.value = this.value.toUpperCase();
for (let i=0; i < options.length; i += 1) {
if (options[i].value == this.value){
document.getElementById('tp_selector').submit();
}
}
};
}
}

View File

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

View File

@ -1,15 +1,3 @@
//
//class Backtesting {
// constructor() {
// this.height = height;
// }
//}
//
//class Trade {
// constructor() {
// this.height = height;
// }
//}
//class Exchange_Info {
// constructor() {
// this.height = height;
@ -21,14 +9,6 @@
// this.height = height;
// }
//}
//
//class Statistics {
// constructor() {
// this.height = height;
// }
//}
//
class User_Interface{
/* This contains all the code for our User interface.
@ -52,11 +32,17 @@ class User_Interface{
this.alerts = new Alerts("alert_list");
/* The object that handles alerts. Pass in the html
element that will hold the list of alerts*/
element that will hold the strategies interface.*/
this.strats = new Strategies("strats_display");
/* These classes interact with HTML elements that need to be parsed first.
TODO: Can any of these objects be created then run init functions after loading?*/
/* The object that handles trades. Pass in the html
element that will hold the trade interface.*/
this.trade = new Trade();
/* This javascript class is loaded at the top of the main html document.
These classes interact with HTML elements that get parsed later in the main html document.
They are wrapped inside a function that executes after the entire document is loaded.
TODO: Decouple these object from these elements. Initialize after first call?.*/
window.addEventListener('load', function () {
/* Charts object is responsible for maintaining the
data visualisation area in the UI. */
@ -87,6 +73,9 @@ class User_Interface{
window.UI.alerts.set_target();
// initialize the strategies instance.
window.UI.strats.initialize();
// initialize the controls instance.
window.UI.controls.init_TP_selector();
window.UI.trade.initialize();
});
}

View File

@ -3,30 +3,36 @@ class Signals {
this.indicators = indicators;
this.signals=[];
}
// Call to display Create new signal dialog.
// Call to display the 'Create new signal' dialog.
open_signal_Form() { document.getElementById("new_sig_form").style.display = "grid"; }
// Call to hide Create new signal dialog.
// Call to hide the 'Create new signal' dialog.
close_signal_Form() { document.getElementById("new_sig_form").style.display = "none"; }
request_signals(){
// Requests a list of all the signals from the server.
window.UI.data.comms.send_to_app('request', 'signals');
}
delete_signal(signal_name){
// Requests that the server remove a specific signal.
window.UI.data.comms.send_to_app('delete_signal', signal_name);
// Get the child element node
// Get the signal element from the UI
let child = document.getElementById(signal_name + '_item');
// Remove the child element from the document
// Ask the parent of the signal element to remove its child(signal) from the document.
child.parentNode.removeChild(child);
}
i_update(updates){
// Update the values listed in the signals section.
// Set local records of incoming signal updates and update the html that displays that info.
for (let signal in this.signals){
// Get the new value of the property from the 1st source in this signal from the update.
let value1 = updates[this.signals[signal].source1].data[0][this.signals[signal].prop1];
// Update the local record of value1
this.signals[signal].value1 = value1.toFixed(2);
if (this.signals[signal].source2 != 'value'){
// Do the same for the second source if the source isn't a set value.
let value2 = updates[this.signals[signal].source2].data[0][this.signals[signal].prop2];
this.signals[signal].value2 = value2.toFixed(2);
}
// Update the html element that displays this information.
document.getElementById(this.signals[signal].name + '_value1').innerHTML = this.signals[signal].value1;
document.getElementById(this.signals[signal].name + '_value2').innerHTML = this.signals[signal].value2;
}
@ -254,5 +260,6 @@ class Signals {
than passing functions, references and callbacks around. */
window.UI.data.comms.send_to_app( "new_signal", data);
this.close_signal_Form();
}
}

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='controls.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>
</head>
<body>
<!-- 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">
<form action="/new_strategy" class="form-container">
<!-- Panel 1 of 1 (5 rows, 2 columns) -->
@ -39,7 +88,7 @@
<!-- Source Input field (row 3/5)-->
<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)">
<option>take_profit</option>
<option>in-out</option>
<option>incremental_profits</option>
<option>swing</option>
</select>
@ -111,7 +160,7 @@
<input type="radio" id="lt" name="Operator" value="<">
<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>
<input type="radio" id="within" name="Operator" value="+/-">
@ -199,6 +248,17 @@
<div id="chart_controls">
<!-- Container target for any indicator output -->
<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 -->
<form action="/settings" method="post">
<input type="hidden" name="setting" value="interval" />
@ -288,16 +348,7 @@
<button class="collapsible bg_blue">Trade</button>
<div class="content">
<div id="trade_content" class="cp_content">
<form action="/trade" method="post">
<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>
<button class="new_btn" id="new_trade" onclick="UI.trade.open_tradeForm()">New Trade</button>
</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 uuid
import requests
from datetime import datetime
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):
# Return a percentage.
if whole == 0:
return 0
pct = 100 * float(part) / float(whole)
return pct
# Set the current value and profit/loss
initial_value = self.position_size * self.opening_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)
# Update the current price.
self.stats['current_price'] = current_price
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:
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.
self.client = client
# For automating limit orders offset the current price by 1/100 percent.
self.offset_amount = 0.0001
# Exchange fees. Maybe these can be fetched from the server?
self.client = None
# Exchange fees. Will be fetched from the server and over writen on connection.
self.exchange_fees = {'maker': 0.01, 'taker': 0.05}
# Hedge mode allows long and shorts to be placed simultaneously.
self.hedge_mode = False
# If hedge mode is disabled this is either {'buy','sell'}.
self.side = None
# A list of unfilled trades.
self.unfilled_trades = []
# A list of filled trades.
self.filled_trades = []
# A completed trades.
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']
# A list of active trades.
self.active_trades = []
# A list of trades that have been closed.
self.settled_trades = []
self.stats = {'num_trades': 0, 'total_position': 0, 'total_position_value': 0}
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 = []
# Check if any unfilled orders are now filled.
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.active_trades:
for trade in self.filled_trades:
# Update the open trades.
ret = trade.update(cdata['close'])
r_update.append({trade.timestamp: {'pl': ret}})
# If a trade has been closed...
if trade.trade_closed:
self.closed_trades.append(trade)
# Notify caller
r_update.append({trade.timestamp: 'closed'})
# Delete closed trades from filled_trades
self.filled_trades[:] = (t for t in self.filled_trades if t.trade_closed is False)
# If the trade hasn't been filled check the status on the exchange.
if trade.status == 'unfilled' or trade.status == 'part-filled':
# Get an updated version of the order from the exchange.
posted_order = self.client.get_order(symbol=trade.symbol, orderId=trade.order.orderId)
# Check to see if the order is filled.
if posted_order.orderStatus == 'FILLED':
# If the order is filled update the trade object.
trade.trade_filled(posted_order.toAmount, posted_order.fromAmount)
else:
# If the order is not filled, do nothing.
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
def new_trade(self, symbol, side, order_type, usd, price=None):
# 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 False
def new_trade(self, target, symbol, side, order_type, qty, price=None, offset=None):
"""
Return a reference to a newly created a Trade object.
# 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:
offset = self.get_price(symbol) * self.offset_amount
if side == 'buy':
price = f'-{offset}'
else:
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 it is proceeded with a +/-, processes as a relative limit order.
if ('+' in price) or ('-' in price):
price = self.get_price(symbol) + float(price)
else:
price = float(price)
# Covert the relative value into an absolute offset of the current price.
price = self.exchange.get_price(symbol) + float(price)
# Convert the string into a float.
price = f_val(price)
position_size = usd / price
# The required level of precision for this trading pair.
precision = self.symbols_n_precision[symbol]
# 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
# position_size is the value of the trade in the parent currency.
# eg .1 BTC @ $10,000 USD/BTC is equal to $1,000 USD.
position_size = f_val(qty * price)
# Update the trades instance.
self.num_trades += 1
self.unfilled_trades.append(trade)
print(order)
return trade
# Log to terminal for development. Todo remove when stable.
print('\nTrades:new_trade(): Creating a trade object.')
print(f'Order amount: {position_size:.6f}')
print(f'Minimum usd: {minimum_n_qty:.2f}')
# 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:
return False
@staticmethod
def get_price(symbol):
# 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 json_obj['price']
def reduce_trade(self, trade_id, qty):
"""
Reduce the position of a trade.
def place_order(self, trade):
if trade.order_type == 'MARKET':
try:
order = self.client.create_test_order(symbol=trade.symbol,
side=trade.side,
type=trade.order_type,
quantity=trade.position_size)
print('!!!Order created!!!')
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}')
:param trade_id: <hex> - The Unique id of a tradee
:param qty: <float> - The quantity to reduce the trade by.
:return: <float> - The base quantity left over.
"""
# Fetch the trade object.
trade = self.get_trade_by_id(trade_id)
if trade.status == 'closed':
# Can't reduce a trade that's been closed.
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}")