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:
parent
934a66012d
commit
79d2a9c597
|
|
@ -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>
|
||||
|
|
@ -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,17 +77,30 @@ 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.
|
||||
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"""
|
||||
with open(filepath, "r") as file_descriptor:
|
||||
|
|
@ -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>
|
||||
try:
|
||||
|
||||
# If file exist load the values.
|
||||
try:
|
||||
self.saved_data = load_configuration(self.config_FN)
|
||||
set_loaded_values()
|
||||
except IOError:
|
||||
# If file doesn't exist create a file and save the default values.
|
||||
except IOError:
|
||||
save_configuration(self.config_FN, self.saved_data)
|
||||
|
||||
elif cmd == 'save':
|
||||
try:
|
||||
# Write saved_data to the file.
|
||||
|
|
|
|||
60
Signals.py
60
Signals.py
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
|
|
@ -51,19 +52,14 @@ class Signal:
|
|||
|
||||
|
||||
class Signals:
|
||||
def __init__(self):
|
||||
self.signals = []
|
||||
# self.set_signals_defaults()
|
||||
def __init__(self, loaded_signals=None):
|
||||
|
||||
def set_signals_defaults(self):
|
||||
"""These defaults are loaded if the config file is not found."""
|
||||
sigs = self.get_signals_defaults()
|
||||
for sig in sigs:
|
||||
self.signals.append(Signal(name=sig['name'], source1=sig['source1'],
|
||||
prop1=sig['prop1'], operator=sig['operator'],
|
||||
source2=sig['source2'], prop2=sig['prop2'],
|
||||
state=sig['state']))
|
||||
return
|
||||
# list of Signal objects.
|
||||
self.signals = []
|
||||
|
||||
# Initialize signals with loaded data.
|
||||
if loaded_signals is not None:
|
||||
self.create_signal_from_dic(loaded_signals)
|
||||
|
||||
@staticmethod
|
||||
def get_signals_defaults():
|
||||
|
|
@ -85,15 +81,41 @@ class Signals:
|
|||
"value2": None, "range": None}
|
||||
return [s1, s2, s3]
|
||||
|
||||
def init_loaded_signals(self, signals_list):
|
||||
for sig in signals_list:
|
||||
self.signals.append(Signal(name=sig['name'], source1=sig['source1'],
|
||||
prop1=sig['prop1'], operator=sig['operator'],
|
||||
source2=sig['source2'], prop2=sig['prop2'],
|
||||
state=sig['state']))
|
||||
def create_signal_from_dic(self, signals_list=None):
|
||||
"""
|
||||
:param signals_list: list of dict
|
||||
:return True: on success.
|
||||
Create and store signal objects from list of dictionaries.
|
||||
"""
|
||||
|
||||
def get_signals(self):
|
||||
# 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:
|
||||
|
|
|
|||
321
Strategies.py
321
Strategies.py
|
|
@ -1,152 +1,145 @@
|
|||
import json
|
||||
|
||||
|
||||
class Strategies:
|
||||
def __init__(self, loaded_strats, trades):
|
||||
self.strat_list = loaded_strats
|
||||
self.trades = trades
|
||||
|
||||
def new_strategy(self, data):
|
||||
self.strat_list.append(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_strategy_by_name(self, name):
|
||||
for obj in self.strat_list:
|
||||
if obj['name'] == name:
|
||||
return obj
|
||||
return False
|
||||
|
||||
def execute_cmd(self, strategy, action, cmd):
|
||||
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')
|
||||
return 'failed'
|
||||
|
||||
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:
|
||||
# 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.')
|
||||
return 'failed'
|
||||
|
||||
def update(self, signals):
|
||||
class Strategy:
|
||||
def __init__(self, **args):
|
||||
"""
|
||||
Receives a reference to updated signal data. Loops through all
|
||||
published strategies and evaluates conditions against the data.
|
||||
This function returns a list of strategies and action commands.
|
||||
:param args: An object containing key_value pairs representing strategy attributes.
|
||||
Strategy format is defined in strategies.js
|
||||
"""
|
||||
# Object containing data to return to function caller.
|
||||
actions = {}
|
||||
# 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:
|
||||
return False
|
||||
self.current_value = None
|
||||
self.opening_value = None
|
||||
self.gross_pl = None
|
||||
self.net_pl = None
|
||||
self.combined_position = None
|
||||
|
||||
def eval_tp_stg(self, strategy, signals):
|
||||
# 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 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):
|
||||
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)
|
||||
if vlue == json.dumps(signal.state):
|
||||
# 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:
|
||||
return False
|
||||
# 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: {strategy['name']}")
|
||||
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):
|
||||
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
|
||||
# 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:
|
||||
return False
|
||||
else:
|
||||
if strategy[condition_type]['typ'] != 'value':
|
||||
raise ValueError('trade_out_condition_met: invalid condition_type')
|
||||
if condition_type == 'take_profit':
|
||||
if self.merged_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
|
||||
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.
|
||||
if strategy['gross_loss'] < strategy['stop_loss']['val']:
|
||||
return True
|
||||
return self.gross_loss < self.stop_loss.val
|
||||
else:
|
||||
return False
|
||||
# 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 = strategy['side']
|
||||
if strategy['side'] == 'buy':
|
||||
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(strategy['trd_in_conds']):
|
||||
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 = strategy['current_position'] + strategy['trade_amount']
|
||||
if proposed_position_size < strategy['max_position']:
|
||||
return 'enter_position', trade_in_cmd
|
||||
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 strategy['active']:
|
||||
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
|
||||
|
|
@ -158,3 +151,115 @@ class Strategies:
|
|||
# No conditions were met.
|
||||
print('Strategies were updated and nothing to do.')
|
||||
return 'do_nothing', 'nothing'
|
||||
|
||||
|
||||
class Strategies:
|
||||
def __init__(self, loaded_strats, trades):
|
||||
# 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):
|
||||
# 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, 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:
|
||||
return obj
|
||||
return False
|
||||
|
||||
def execute_cmd(self, strategy, action, cmd):
|
||||
order_type = 'LIMIT'
|
||||
if action == 'open_position':
|
||||
# Attempt to create the 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.
|
||||
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'
|
||||
|
||||
print(f'Strategies.execute_cmd: Invalid action received: {action}')
|
||||
return 'failed'
|
||||
|
||||
def update(self, signals):
|
||||
"""
|
||||
Receives a reference to updated signal data. Loops through all
|
||||
published strategies and evaluates conditions against the data.
|
||||
This function returns a list of strategies and action commands.
|
||||
"""
|
||||
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:
|
||||
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
|
||||
else:
|
||||
return return_obj
|
||||
|
||||
|
|
|
|||
157
app.py
157
app.py
|
|
@ -1,28 +1,33 @@
|
|||
import json
|
||||
|
||||
# Flask is a lightweight html server used to render the UI.
|
||||
from flask import Flask, render_template, request, redirect, jsonify
|
||||
from flask_cors import cross_origin
|
||||
from binance.enums import *
|
||||
from flask_sock import Sock
|
||||
|
||||
# Handles all server side data and interactions.
|
||||
# Handles all updates and requests for locally stored data.
|
||||
from data import BrighterData
|
||||
|
||||
# Define app
|
||||
app = Flask(__name__)
|
||||
sock = Sock(app)
|
||||
# Create a Flask object named interface that serves the html.
|
||||
interface = Flask(__name__)
|
||||
# Create a socket in order to receive requests.
|
||||
sock = Sock(interface)
|
||||
|
||||
# This object maintains all the application and historical data.
|
||||
# Access to server, local storage, other classes go through here.
|
||||
# Create a BrighterData object. This the main application that maintains access to the server, local storage,
|
||||
# and manages objects that process trade data.
|
||||
app_data = BrighterData()
|
||||
|
||||
# app.config['SECRET_KEY'] = 'The quick brown fox jumps over the lazy dog'
|
||||
# app.config['CORS_HEADERS'] = 'Content-Type'
|
||||
# cors = CORS(app, resources={r"*": {"origins": "*"}})
|
||||
|
||||
@app.route('/')
|
||||
# TODO: The cors object had something to do with an error I was getting while
|
||||
# receiving json. It may not be needed anymore.
|
||||
# interface.config['CORS_HEADERS'] = 'Content-Type'
|
||||
# cors = CORS(interface, resources={r"*": {"origins": "*"}})
|
||||
|
||||
@interface.route('/')
|
||||
def index():
|
||||
# Passes data into an HTML template and serves it to a locally hosted server
|
||||
# Generate and return a landing page for the web application.
|
||||
# Fetch data from app_data and inject it into an HTML template.
|
||||
# Render the template and serve it upon request.
|
||||
rendered_data = app_data.get_rendered_data()
|
||||
js_data = app_data.get_js_init_data()
|
||||
return render_template('index.html',
|
||||
|
|
@ -40,10 +45,18 @@ 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 'message_type' not in msg_obj:
|
||||
return
|
||||
|
||||
if msg_obj['message_type'] == 'candle_data':
|
||||
# Send the candle to the BrighterData_obj
|
||||
# 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:
|
||||
|
|
@ -54,6 +67,8 @@ def ws(sock):
|
|||
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:
|
||||
|
|
@ -72,22 +87,29 @@ def ws(sock):
|
|||
}
|
||||
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'] == 'reply':
|
||||
print(msg_obj['rep'])
|
||||
print('Reply')
|
||||
|
||||
if msg_obj['message_type'] == 'new_signal':
|
||||
# Send the data to the BrighterData_obj
|
||||
# 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:
|
||||
|
|
@ -109,6 +131,23 @@ def ws(sock):
|
|||
}
|
||||
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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
23
candles.py
23
candles.py
|
|
@ -1,24 +1,30 @@
|
|||
import datetime
|
||||
import csv
|
||||
from binance.enums import HistoricalKlinesType
|
||||
|
||||
|
||||
class Candles:
|
||||
def __init__(self, config, client):
|
||||
# Keep a reference to the exchange client.
|
||||
self.client = client
|
||||
def __init__(self, config, exchange):
|
||||
# Keep a reference to the exchange object.
|
||||
self.exchange = exchange
|
||||
|
||||
# The maximum amount of data to load into memory at one time.
|
||||
self.max_data_loaded = config.max_data_loaded
|
||||
# A reference to the app configuration
|
||||
|
||||
# A reference to the interface configuration
|
||||
self.config = config
|
||||
|
||||
# The entire loaded candle history
|
||||
self.candlesticks = []
|
||||
|
||||
# List of dictionaries of timestamped high, low, and closing values
|
||||
self.latest_high_values = []
|
||||
self.latest_low_values = []
|
||||
self.latest_close_values = []
|
||||
|
||||
# Values of the last candle received
|
||||
self.last_candle = None
|
||||
|
||||
# List of dictionaries of timestamped volume values
|
||||
self.latest_vol = []
|
||||
|
||||
|
|
@ -26,11 +32,15 @@ class Candles:
|
|||
self.set_candle_history(symbol=config.trading_pair, interval=config.chart_interval)
|
||||
|
||||
def set_new_candle(self, cdata):
|
||||
# TODO: this is not updating self.candlesticks[]. I think it is because it is
|
||||
# todo: not in the raw format received earlier. check that the format is the same.
|
||||
# todo: candlesticks is accessed by get_candle_history() while init the charts
|
||||
self.last_candle = cdata
|
||||
self.latest_close_values.append({'time': cdata['time'], 'close': cdata['close']})
|
||||
self.latest_high_values.append({'time': cdata['time'], 'high': cdata['high']})
|
||||
self.latest_low_values.append({'time': cdata['time'], 'low': cdata['low']})
|
||||
self.latest_vol.append({'time': cdata['time'], 'value': cdata['vol']})
|
||||
return True
|
||||
|
||||
def load_candle_history(self, symbol, interval):
|
||||
""" Retrieve candlestick history from a file and append it with
|
||||
|
|
@ -76,7 +86,10 @@ class Candles:
|
|||
# Set <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
176
data.py
|
|
@ -1,10 +1,7 @@
|
|||
from binance.client import Client
|
||||
|
||||
import config
|
||||
from Strategies import Strategies
|
||||
from candles import Candles
|
||||
from Configuration import Configuration
|
||||
from exchange_info import ExchangeInfo
|
||||
from exchange import Exchange
|
||||
from indicators import Indicators
|
||||
from Signals import Signals
|
||||
from trade import Trades
|
||||
|
|
@ -14,39 +11,34 @@ import json
|
|||
class BrighterData:
|
||||
def __init__(self):
|
||||
|
||||
# Initialise a connection to the Binance client API
|
||||
self.client = Client(config.API_KEY, config.API_SECRET)
|
||||
|
||||
# Object that maintains signals
|
||||
self.signals = Signals()
|
||||
# Object that interacts and maintains exchange and account data
|
||||
self.exchange = Exchange()
|
||||
|
||||
# Configuration and settings for the user interface and charts
|
||||
self.config = Configuration()
|
||||
|
||||
# Load any saved data from file
|
||||
self.config.config_and_states('load')
|
||||
|
||||
# Initialize signals with loaded data.
|
||||
self.signals.init_loaded_signals(self.config.signals_list)
|
||||
# Object that maintains signals. Initialize with any signals loaded from file.
|
||||
self.signals = Signals(self.config.signals_list)
|
||||
|
||||
# Object that maintains candlestick and price data.
|
||||
self.candles = Candles(self.config, self.client)
|
||||
self.candles = Candles(self.config, self.exchange)
|
||||
|
||||
# Object that interacts with and maintains data from available indicators
|
||||
self.indicators = Indicators(self.candles, self.config)
|
||||
|
||||
# Object that maintains exchange and account data
|
||||
self.exchange_info = ExchangeInfo(self.client)
|
||||
|
||||
# Object that maintains the strategies data
|
||||
self.trades = Trades(self.client)
|
||||
# Object that maintains the trades data
|
||||
self.trades = Trades(self.config.trades)
|
||||
# The Trades object needs to connect to an exchange.
|
||||
self.trades.connect_exchange(exchange=self.exchange)
|
||||
|
||||
# Object that maintains the strategies data
|
||||
self.strategies = Strategies(self.config.strategies_list, self.trades)
|
||||
|
||||
def get_js_init_data(self):
|
||||
"""Returns a JSON object of initialization data
|
||||
for the javascript in the rendered HTML"""
|
||||
"""
|
||||
Returns a JSON object of initialization data.
|
||||
This is passed into the frontend HTML template for the javascript to access in the rendered HTML.
|
||||
"""
|
||||
js_data = {'i_types': self.indicators.indicator_types,
|
||||
'indicators': self.indicators.indicator_list,
|
||||
'interval': self.config.chart_interval,
|
||||
|
|
@ -55,13 +47,13 @@ class BrighterData:
|
|||
|
||||
def get_rendered_data(self):
|
||||
"""
|
||||
Data to be rendered in the HTML
|
||||
Data to be injected into the HTML template that renders the frontend UI.
|
||||
"""
|
||||
rd = {}
|
||||
rd['title'] = self.config.application_title # Title of the page
|
||||
rd['my_balances'] = self.exchange_info.balances # Balances on the exchange
|
||||
rd['symbols'] = self.exchange_info.symbols # Symbols information from the exchange
|
||||
rd['intervals'] = self.exchange_info.intervals # Time candle time intervals available to stream
|
||||
rd['my_balances'] = self.exchange.balances # Balances on the exchange
|
||||
rd['symbols'] = self.exchange.symbols # Symbols information from the exchange
|
||||
rd['intervals'] = self.exchange.intervals # Time candle time intervals available to stream
|
||||
rd['chart_interval'] = self.config.chart_interval # The charts current interval setting
|
||||
rd['indicator_types'] = self.indicators.indicator_types # All the types indicators Available
|
||||
rd['indicator_list'] = self.indicators.get_indicator_list() # indicators available
|
||||
|
|
@ -70,83 +62,96 @@ class BrighterData:
|
|||
return rd
|
||||
|
||||
def received_cdata(self, cdata):
|
||||
# If this is the first candle received,
|
||||
# then set last_candle. Then set a new candle.
|
||||
"""
|
||||
This is called to pass in new price data when it is received.
|
||||
:param cdata: <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')
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
class Backtesting {
|
||||
constructor() {
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class Statistics {
|
||||
constructor() {
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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__)
|
||||
792
trade.py
792
trade.py
|
|
@ -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")
|
||||
: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)
|
||||
|
||||
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
|
||||
# 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:
|
||||
print(f'Trade: No Implementation for trade.order: {trade.order_type}')
|
||||
return None
|
||||
# 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}")
|
||||
|
|
|
|||
Loading…
Reference in New Issue