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 = 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