def __init__(self, data=None, flavor='mysql', lvl='debug'): #NOTE Allowing different data access ? self.data = data if not data: self.feeds = DataFeed() self.backtest_cfg = None self.algo_cfg = None self.manager_cfg = None self.monthly_perfs = None self.server = ZMQ_Dealer(id=self.__class__.__name__)
def genetic(domain, cost_obj, popsize=50, step=1, mutprob=0.2, elite=0.2, maxiter=100, stop=0, notify_android=False): ''' Parameter optimization using genetic algorithm ______________________________________________ Parameters domain: vec(...) of tuple(2) define range for each parameter cost_obj: Metric(1) compute the score of a solution popsize: int(1) number of solution set in one generation step: float(1) sensibility used while mutating a parameter mutprob: float(1) probability for a solution to mutate elite: float(1) % of best chromosomes selected maxiter: int(1) maximum number of population evolution stop: float(1) stop the algorithm when the fitness function reachs this value notify_android: bool(1) flag that let you send a notificatioin on android device when the algo is done ______________________________________________ Return scores[0][0]: float(1) Best score the algorithm reached scores[0][1]: vec(...) of float(1) parameters that gave the best score ''' # Initialisation client = ZMQ_Dealer(id=genetic.__name__) client.run(host='127.0.0.1', port=5570) check_buffer = [1] * 4 # Mutation Operation def mutate(vec): i = random.randint(0, len(domain) - 1) if random.random() < 0.5 and vec[i] > domain[i][0]: mutated_param = vec[i] - step if vec[i] - step >= domain[i][0] else domain[i][0] elif vec[i] < domain[i][1]: mutated_param = vec[i] + step if vec[i] + step <= domain[i][0] else domain[i][0] return vec[0:i] + [mutated_param] + vec[i + 1:] # Crossover Operation def crossover(r1, r2): i = random.randint(1, len(domain) - 2) return r1[0:i] + r2[i:] def should_stop(best): ''' Break the loop if no longer evolution, or reached stop criteria ''' check_buffer.append(best) check_buffer.pop(0) return (best >= check_buffer[0]) or (best <= stop) log.info('Build the initial population') pop = [] for i in range(popsize): vec = [random.randint(domain[i][0], domain[i][1]) for i in range(len(domain))] pop.append(vec) # How many winners from each generation? topelite = int(elite * popsize) log.info('Run main loop') for i in range(maxiter): log.info('Rank population') scores = [(cost_obj.fitness(v), v) for v in pop] scores.sort() ranked = [v for (s, v) in scores] # Start with the pure winners log.info('Select elite') pop = ranked[0:topelite] # Add mutated and bred forms of the winners log.info('Evolve loosers') while len(pop) < popsize: if random.random() < mutprob: log.debug('Process mutation') c = random.randint(0, topelite) pop.append(mutate(ranked[c])) else: log.debug('Process crossover') c1 = random.randint(0, topelite) c2 = random.randint(0, topelite) pop.append(crossover(ranked[c1], ranked[c2])) #TODO add worst log.error(scores) log.notice('Best score so far: {}'.format(scores[0][0])) client.send({'best': scores[0], 'worst': scores[-1], 'parameters': scores[0][1], 'mean': np.mean([s[0] for s in scores]), 'std': np.std([s[0] for s in scores]), 'iteration': i + 1, 'progress': round(float(i + 1) / float(maxiter), 2) * 100.0}, type='optimization', channel='dashboard') if should_stop(scores[0][0]): log.info('Stop criteria reached, done with optimization.') break if notify_android: client.send_to_android({'title': 'Optimization is done', 'priority': 1, 'description': 'Genetic algorithm evolved the solution to {} \ (with parameters {})'.format(scores[0][0], scores[0][1])}) return scores[0][0], scores[0][1]
class Simulation(object): ''' Take a trading strategie and evalute its results ''' def __init__(self, data=None, flavor='mysql', lvl='debug'): #NOTE Allowing different data access ? self.data = data if not data: self.feeds = DataFeed() self.backtest_cfg = None self.algo_cfg = None self.manager_cfg = None self.monthly_perfs = None self.server = ZMQ_Dealer(id=self.__class__.__name__) def configure(self, bt_cfg=None, a_cfg=None, m_cfg=None): ''' Reads and provides a clean configuration for the simulation ''' if not bt_cfg: parser = argparse.ArgumentParser(description='Backtester module, the terrific financial simukation') parser.add_argument('-v', '--version', action='version', version='%(prog)s v0.8.1 Licence rien du tout', help='Print program version') parser.add_argument('-d', '--delta', type=str, action='store', default='D', required=False, help='Delta in days betweend two quotes fetch') parser.add_argument('-a', '--algorithm', action='store', required=True, help='Trading algorithm to be used') parser.add_argument('-m', '--manager', action='store', required=True, help='Portfolio strategie to be used') parser.add_argument('-b', '--database', action='store', default='test', required=False, help='Table to considere in database') parser.add_argument('-t', '--tickers', action='store', required=True, help='target names to process') parser.add_argument('-s', '--start', action='store', default='1/1/2006', required=False, help='Start date of the backtester') parser.add_argument('-e', '--end', action='store', default='1/12/2010', required=False, help='Stop date of the backtester') parser.add_argument('-r', '--remote', action='store_true', help='Indicates if the program was ran manually or not') parser.add_argument('-l', '--live', action='store_true', help='makes the engine work in real-time !') parser.add_argument('-p', '--port', action='store', default=5570, required=False, help='Activates the diffusion of the universe on the network, on the port provided') args = parser.parse_args() self.backtest_cfg = {'algorithm' : args.algorithm, 'delta' : args.delta, 'manager' : args.manager, 'database' : args.database, 'tickers' : args.tickers.split(','), 'start' : args.start, 'end' : args.end, 'live' : args.live, 'port' : args.port, 'remote' : args.remote} else: self.backtest_cfg = bt_cfg if isinstance(self.backtest_cfg['start'], str) and isinstance(self.backtest_cfg['end'], str): ###############test = pd.datetime.strptime('2012-02-01:14:00', '%Y-%m-%d:%H:%M') self.backtest_cfg['start'] = pytz.utc.localize(pd.datetime.strptime(self.backtest_cfg['start'], '%Y-%m-%d')) self.backtest_cfg['end'] = pytz.utc.localize(pd.datetime.strptime(self.backtest_cfg['end'], '%Y-%m-%d')) elif isinstance(self.backtest_cfg['start'], pd.datetime) and isinstance(self.backtest_cfg['end'], pd.datetime): if not self.backtest_cfg['start'].tzinfo: self.backtest_cfg['start'] = pytz.utc.localize(self.backtest_cfg['start']) if not self.backtest_cfg['end'].tzinfo: self.backtest_cfg['end'] = pytz.utc.localize(self.backtest_cfg['end']) else: raise NotImplementedError() #if self.backtest_cfg['interactive']: self.read_config(remote=self.backtest_cfg['remote'], manager=m_cfg, algorithm=a_cfg) #else: #self.server.run_forever(port=self.backtest_cfg['port'], on_recv=self.read_config) return self.backtest_cfg def read_config(self, *args, **kwargs): self.manager_cfg = kwargs.get('manager', None) self.algo_cfg = kwargs.get('algorithm', None) if kwargs.get('remote', False): self.server.run(host='127.0.0.1', port=self.backtest_cfg['port']) # In remote mode, client sends missing configuration through zmq socket if (not self.manager_cfg) or (not self.algo_cfg): log.info('Fetching backtest configuration from client') msg = self.server.receive(json=True) log.debug('Got it !') # Set simulation parameters with it assert isinstance(msg, dict) if self.manager_cfg is None: self.manager_cfg = msg['manager'] if self.algo_cfg is None: self.algo_cfg = msg['algorithm'] else: # Reading configuration from json files config_dir = '/'.join((os.environ['QTRADE'], 'config/')) try: # Files store many algos and manager parameters, # use backtest configuration to pick up the right one if self.manager_cfg is None: self.manager_cfg = json.load(open('{}/managers.cfg'.format(config_dir), 'r'))[self.backtest_cfg['manager']] if self.algo_cfg is None: self.algo_cfg = json.load(open('{}/algos.cfg'.format(config_dir), 'r'))[self.backtest_cfg['algorithm']] except: log.error('** loading json configuration.') sys.exit(1) # The manager can use the same socket during simulation to emit portfolio informations self.manager_cfg['server'] = self.server log.info('Configuration is Done.') def run_backtest(self): # No configuration, no backtest man if self.backtest_cfg is None or self.algo_cfg is None or self.manager_cfg is None: log.error('** Backtester not configured properly') return 1 #'''_____________________________________________________________________ Parameters ________ if self.backtest_cfg['tickers'][0] == 'random': assert(len(self.backtest_cfg['tickers']) == 2) assert(int(self.backtest_cfg['tickers'][1])) self.backtest_cfg['tickers'] = self.feeds.random_stocks(int(self.backtest_cfg['tickers'][1])) if self.backtest_cfg['live']: #dates = pd.date_range(self.backtest_cfg['start'], self.backtest_cfg['end'], freq=self.backtest_cfg['delta']) #NOTE A temporary hack to avoid zipline dirty modification periods = self.backtest_cfg['end'] - self.backtest_cfg['start'] dates = pd.date_range(pd.datetime.now(), periods=periods.days + 1, freq=self.backtest_cfg['delta']) ''' if dates.freq > pd.datetools.Day(): #fr_selector = ((dates.hour > 8) & (dates.hour < 17)) | ((dates.hour == 17) & (dates.minute < 31)) us_selector = ((dates.hour > 15) & (dates.hour < 22)) | ((dates.hour == 15) & (dates.minute > 31)) dates = dates[us_selector] if not dates: log.warning('! Market closed.') sys.exit(0) ''' data = {'tickers': self.backtest_cfg['tickers'], 'index': dates.tz_localize(pytz.utc)} else: data = self.feeds.quotes(self.backtest_cfg['tickers'], start_date = self.backtest_cfg['start'], end_date = self.backtest_cfg['end']) assert isinstance(data, pd.DataFrame) assert data.index.tzinfo #___________________________________________________________________________ Running ________ log.info('\n-- Running backetester...\nUsing algorithm: {}\n'.format(self.backtest_cfg['algorithm'])) log.info('\n-- Using portfolio manager: {}\n'.format(self.backtest_cfg['manager'])) backtester = BacktesterEngine(self.backtest_cfg['algorithm'], self.backtest_cfg['manager'], self.algo_cfg, self.manager_cfg) self.results, self.monthly_perfs = backtester.run(data) #self.backtest_cfg['start'], #self.backtest_cfg['end']) return self.results def rolling_performances(self, timestamp='one_month', save=False, db_id=None): ''' Filters self.perfs and, if asked, save it to database ''' #TODO Study the impact of month choice #TODO Check timestamp in an enumeration #NOTE Script args define default database table name (test), make it consistent if db_id is None: db_id = self.backtest_cfg['algorithm'] + pd.datetime.strftime(pd.datetime.now(), format='%Y%m%d') if self.monthly_perfs: perfs = dict() length = range(len(self.monthly_perfs[timestamp])) index = self._get_index(self.monthly_perfs[timestamp]) perfs['Name'] = np.array([db_id] * len(self.monthly_perfs[timestamp])) #perfs['Period'] = np.array([self.monthly_perfs[timestamp][i]['period_label'] for i in length]) perfs['Period'] = np.array([pd.datetime.date(date) for date in index]) perfs['Sharpe.Ratio'] = np.array([self.monthly_perfs[timestamp][i]['sharpe'] for i in length]) perfs['Returns'] = np.array([self.monthly_perfs[timestamp][i]['algorithm_period_return'] for i in length]) perfs['Max.Drawdown'] = np.array([self.monthly_perfs[timestamp][i]['max_drawdown'] for i in length]) perfs['Volatility'] = np.array([self.monthly_perfs[timestamp][i]['algo_volatility'] for i in length]) perfs['Beta'] = np.array([self.monthly_perfs[timestamp][i]['beta'] for i in length]) perfs['Alpha'] = np.array([self.monthly_perfs[timestamp][i]['alpha'] for i in length]) perfs['Excess.Returns'] = np.array([self.monthly_perfs[timestamp][i]['excess_return'] for i in length]) perfs['Benchmark.Returns'] = np.array([self.monthly_perfs[timestamp][i]['benchmark_period_return'] for i in length]) perfs['Benchmark.Volatility'] = np.array([self.monthly_perfs[timestamp][i]['benchmark_volatility'] for i in length]) perfs['Treasury.Returns'] = np.array([self.monthly_perfs[timestamp][i]['treasury_period_return'] for i in length]) else: #TODO Get it from DB if it exists raise NotImplementedError() try: data = pd.DataFrame(perfs, index=index) except: import ipdb; ipdb.set_trace() if save: self.feeds.stock_db.save_metrics(data) return data def overall_metrics(self, timestamp='one_month', save=False, db_id=None): ''' Use zipline results to compute some performance indicators and store it in database ''' perfs = dict() metrics = self.rolling_performances(timestamp=timestamp, save=False, db_id=db_id) riskfree = np.mean(metrics['Treasury.Returns']) #NOTE Script args define default database table name (test), make it consistent if db_id is None: db_id = self.backtest_cfg['algorithm'] + pd.datetime.strftime(pd.datetime.now(), format='%Y%m%d') perfs['Name'] = db_id perfs['Sharpe.Ratio'] = tsu.get_sharpe_ratio(metrics['Returns'].values, risk_free = riskfree) perfs['Returns'] = (((metrics['Returns'] + 1).cumprod()) - 1)[-1] perfs['Max.Drawdown'] = max(metrics['Max.Drawdown']) perfs['Volatility'] = np.mean(metrics['Volatility']) perfs['Beta'] = np.mean(metrics['Beta']) perfs['Alpha'] = np.mean(metrics['Alpha']) perfs['Benchmark.Returns'] = (((metrics['Benchmark.Returns'] + 1).cumprod()) - 1)[-1] if save: self.feeds.stock_db.save_performances(perfs) return perfs #TODO Save returns def get_returns(self, benchmark=None, timestamp='one_month', save=False, db_id=None): returns = dict() if benchmark: #TODO Benchmark fields in database for guessing name like for stocks benchmark_symbol = '^GSPC' benchmark_data = get_benchmark_returns(benchmark_symbol, self.backtest_cfg['start'], self.backtest_cfg['end']) else: raise NotImplementedError() #benchmark_data = [d for d in benchmark_data if (d.date >= self.backtest_cfg['start']) and (d.date <= self.backtest_cfg['end'])] dates = pd.DatetimeIndex([d.date for d in benchmark_data]) returns['Benchmark.Returns'] = pd.Series([d.returns for d in benchmark_data], index=dates) returns['Benchmark.CReturns'] = ((returns['Benchmark.Returns'] + 1).cumprod()) - 1 returns['Returns'] = pd.Series(self.results.returns, index=dates) returns['CReturns'] = pd.Series(((self.results.returns + 1).cumprod()) - 1, index=dates) df = pd.DataFrame(returns, index=dates) if save: raise NotImplementedError() self.feeds.stock_db.saveDFToDB(df, table=db_id) if benchmark is None: df = df.drop(['Benchmark.Returns', 'Benchmark.CReturns'], axis=1) return df def _get_index(self, perfs): #NOTE No frequency infos or just period number ? start = pytz.utc.localize(pd.datetime.strptime(perfs[0]['period_label'] + '-01', '%Y-%m-%d')) end = pytz.utc.localize(pd.datetime.strptime(perfs[-1]['period_label'] + '-01', '%Y-%m-%d')) return pd.date_range(start - pd.datetools.BDay(10), end, freq=pd.datetools.MonthBegin()) def _extract_perf(self, perfs, field): index = self._get_index(perfs) values = [perfs[i][field] for i in range(len(perfs))] return pd.Series(values, index=index) def __del__(self): del self.server
def genetic_optimize(domain, cost_obj, popsize=50, step=1, mutprob=0.2, elite=0.2, maxiter=100, stop=0, notify_android=False): ''' Parameter optimization using genetic algorithm ______________________________________________ Parameters domain: vec(...) of tuple(2) define range for each parameter cost_obj: Metric(1) compute the score of a solution popsize: int(1) number of solution set in one generation step: float(1) sensibility used while mutating a parameter mutprob: float(1) probability for a solution to mutate elite: float(1) % of best chromosomes selected maxiter: int(1) maximum number of population evolution stop: float(1) stop the algorithm when the fitness function reachs this value notify_android: bool(1) flag that let you send a notificatioin on android device when the algo is done ______________________________________________ Return scores[0][0]: float(1) Best score the algorithm reached scores[0][1]: vec(...) of float(1) parameters that gave the best score ''' # Initialisation client = ZMQ_Dealer(id=genetic_optimize.__name__) client.run(host='127.0.0.1', port=5570) check_buffer = [1] * 4 # Mutation Operation def mutate(vec): i = random.randint(0, len(domain) - 1) if random.random() < 0.5 and vec[i] > domain[i][0]: mutated_param = vec[i] - step if vec[i] - step >= domain[i][ 0] else domain[i][0] elif vec[i] < domain[i][1]: mutated_param = vec[i] + step if vec[i] + step <= domain[i][ 0] else domain[i][0] return vec[0:i] + [mutated_param] + vec[i + 1:] # Crossover Operation def crossover(r1, r2): i = random.randint(1, len(domain) - 2) return r1[0:i] + r2[i:] def should_stop(best): ''' Break the loop if no longer evolution, or reached stop criteria ''' check_buffer.append(best) check_buffer.pop(0) return (best >= check_buffer[0]) or (best <= stop) log.info('Build the initial population') pop = [] for i in range(popsize): vec = [ random.randint(domain[i][0], domain[i][1]) for i in range(len(domain)) ] pop.append(vec) # How many winners from each generation? topelite = int(elite * popsize) log.info('Run main loop') for i in range(maxiter): log.info('Rank population') scores = [(cost_obj.fitness(v), v) for v in pop] scores.sort() ranked = [v for (s, v) in scores] # Start with the pure winners log.info('Select elite') pop = ranked[0:topelite] # Add mutated and bred forms of the winners log.info('Evolve loosers') while len(pop) < popsize: if random.random() < mutprob: log.debug('Process mutation') # Mutation c = random.randint(0, topelite) pop.append(mutate(ranked[c])) else: # Crossover log.debug('Process crossover') c1 = random.randint(0, topelite) c2 = random.randint(0, topelite) pop.append(crossover(ranked[c1], ranked[c2])) #TODO add worst log.error(scores) log.notice('Best score so far: {}'.format(scores[0][0])) client.send( { 'best': scores[0], 'worst': scores[-1], 'parameters': scores[0][1], 'mean': np.mean([s[0] for s in scores]), 'std': np.std([s[0] for s in scores]), 'iteration': i + 1, 'progress': round(float(i + 1) / float(maxiter), 2) * 100.0 }, type='optimization', channel='dashboard') if should_stop(scores[0][0]): log.info('Stop criteria reached, done with optimization.') break if notify_android: client.send_to_android({ 'title': 'Optimization done', 'priority': 1, 'description': 'Genetic algorithm evolved the solution to {} \ (with parameters {})'.format( scores[0][0], scores[0][1]) }) return scores[0][0], scores[0][1]