266 lines
11 KiB
Python
266 lines
11 KiB
Python
import json
|
|
|
|
|
|
class Strategy:
|
|
def __init__(self, **args):
|
|
"""
|
|
:param args: An object containing key_value pairs representing strategy attributes.
|
|
Strategy format is defined in strategies.js
|
|
"""
|
|
self.current_value = None
|
|
self.opening_value = None
|
|
self.gross_pl = None
|
|
self.net_pl = None
|
|
self.combined_position = None
|
|
|
|
# A strategy is defined in Strategies.js it is received from the client,
|
|
# then unpacked and converted into a python object here.
|
|
for name, value in args.items():
|
|
# Make each keyword-argument a property of the class.
|
|
setattr(self, name, value)
|
|
|
|
# A container to hold previous state of signals.
|
|
self.last_states = {}
|
|
|
|
# A list of all the trades made by this strategy.
|
|
self.trades = []
|
|
|
|
def get_position(self):
|
|
return self.combined_position
|
|
|
|
def get_pl(self):
|
|
self.update_pl()
|
|
return self.net_pl
|
|
|
|
def update_pl(self):
|
|
# sum the pl of all the trades.
|
|
position_sum = 0
|
|
pl_sum = 0
|
|
opening_value_sum = 0
|
|
value_sum = 0
|
|
for trade in self.trades:
|
|
pl_sum += trade.profit_loss
|
|
position_sum += trade.position_size
|
|
value_sum += trade.value
|
|
opening_value_sum += trade.opening_value
|
|
self.combined_position = position_sum
|
|
self.gross_pl = pl_sum
|
|
self.opening_value = opening_value_sum
|
|
self.current_value = value_sum
|
|
|
|
def to_json(self):
|
|
return json.dumps(self, default=lambda o: o.__dict__,
|
|
sort_keys=True, indent=4)
|
|
|
|
def evaluate_strategy(self, signals):
|
|
"""
|
|
:param signals: Signals: A reference to an object that handles current signal states.
|
|
:return action: Action required based on evaluation. format{cmd:str, amount:real, margin:int}
|
|
"""
|
|
|
|
def condition_satisfied(sig_name, value):
|
|
"""
|
|
Check if a signal has a state of value.
|
|
:param sig_name: str: The name of a signal object to compare states.
|
|
:param value: The state value to compare.
|
|
:return bool: True: <Signal:sig_name:state> == <value>.
|
|
"""
|
|
signal = signals.get_signal_by_name(sig_name)
|
|
# Evaluate for a state change
|
|
if value == 'changed':
|
|
# Store the state if it hasn't been stored yet.
|
|
if sig_name not in self.last_states:
|
|
self.last_states.update({sig_name: signal.state})
|
|
# Store the new state and return true if the state has changed.
|
|
if self.last_states[sig_name] != signal.state:
|
|
self.last_states.update({sig_name: signal.state})
|
|
return True
|
|
else:
|
|
# Else return true if the values match.
|
|
return value == json.dumps(signal.state)
|
|
|
|
def all_conditions_met(conditions):
|
|
# Loops through a lists of signal names and states.
|
|
# Returns True if all combinations are true.
|
|
if len(conditions) < 1:
|
|
print(f"no trade-in conditions supplied: {self.name}")
|
|
return False
|
|
# Evaluate all conditions and return false if any are un-met.
|
|
for trigger_signal in conditions.keys():
|
|
trigger_value = conditions[trigger_signal]
|
|
# Compare this signal's state with the trigger_value
|
|
print(f'evaluating :({trigger_signal, trigger_value})')
|
|
if not condition_satisfied(trigger_signal, trigger_value):
|
|
print('returning false')
|
|
return False
|
|
print('all conditions met!!!')
|
|
return True
|
|
|
|
def trade_out_condition_met(condition_type):
|
|
# Retrieve the condition from either the 'stop_loss' or 'take_profit' obj.
|
|
condition = getattr(self, condition_type)
|
|
# Subtypes of conditions are 'conditional' or 'value'.
|
|
if condition.typ == 'conditional':
|
|
signal_name = condition.trig
|
|
signal_value = condition.val
|
|
return condition_satisfied(signal_name, signal_value)
|
|
else:
|
|
if condition_type == 'take_profit':
|
|
if self.merged_profit:
|
|
# If the profit condition is met send command to take profit.
|
|
return self.gross_profit > self.take_profit.val
|
|
else:
|
|
# Loop through each associated trade and test
|
|
for trade in self.trades:
|
|
return trade.profit_loss > self.take_profit.val
|
|
elif condition_type == 'value':
|
|
if self.merged_loss:
|
|
# If the loss condition is met, return a trade-out command.
|
|
return self.gross_loss < self.stop_loss.val
|
|
else:
|
|
# Loop through each associated trade and test
|
|
for trade in self.trades:
|
|
return trade.profit_loss < self.stop_loss.val
|
|
else:
|
|
raise ValueError('trade_out_condition_met: invalid condition_type')
|
|
|
|
trade_in_cmd = self.side
|
|
if self.side == 'buy':
|
|
trade_out_cmd = 'sell'
|
|
else:
|
|
trade_out_cmd = 'buy'
|
|
if self.type == 'in-out':
|
|
print('evaluating trade in conditions for in / out')
|
|
# If trade-in conditions are met.
|
|
if all_conditions_met(self.trd_in_conds):
|
|
# If the new trade wouldn't exceed max_position. Return a trade-in command.
|
|
proposed_position_size = int(self.combined_position) + int(self.trade_amount)
|
|
if proposed_position_size < int(self.max_position):
|
|
return 'open_position', trade_in_cmd
|
|
|
|
# If strategy is active test the take-profit or stop-loss conditions.
|
|
if self.active:
|
|
# Conditional take-profit trades-out if a signals equals a set value.
|
|
if trade_out_condition_met('take_profit'):
|
|
return 'take_profit', trade_out_cmd
|
|
|
|
# Conditional stop-loss trades-outs if a signals value equals a set value.
|
|
if trade_out_condition_met('stop_loss'):
|
|
return 'stop_loss', trade_out_cmd
|
|
|
|
# No conditions were met.
|
|
print('Strategies were updated and nothing to do.')
|
|
return 'do_nothing', 'nothing'
|
|
|
|
|
|
class Strategies:
|
|
def __init__(self, loaded_strats, trades):
|
|
# 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
|
|
|