brighter-trading/Strategies.py

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