def test_orders_stop(self, name, order_data, event_data, expected): data = order_data data['asset'] = self.ASSET133 order = Order(**data) if expected['transaction']: expected['transaction']['asset'] = self.ASSET133 event_data['asset'] = self.ASSET133 assets = ((133, pd.DataFrame( { 'open': [event_data['open']], 'high': [event_data['high']], 'low': [event_data['low']], 'close': [event_data['close']], 'volume': [event_data['volume']], }, index=[pd.Timestamp('2006-01-05 14:31', tz='UTC')], )), ) days = pd.date_range(start=normalize_date(self.minutes[0]), end=normalize_date(self.minutes[-1])) with tmp_bcolz_equity_minute_bar_reader(self.trading_calendar, days, assets) as reader: data_portal = DataPortal( self.env.asset_finder, self.trading_calendar, first_trading_day=reader.first_trading_day, equity_minute_reader=reader, ) slippage_model = VolumeShareSlippage() try: dt = pd.Timestamp('2006-01-05 14:31', tz='UTC') bar_data = BarData( data_portal, lambda: dt, self.sim_params.data_frequency, self.trading_calendar, NoRestrictions(), ) _, txn = next( slippage_model.simulate( bar_data, self.ASSET133, [order], )) except StopIteration: txn = None if expected['transaction'] is None: self.assertIsNone(txn) else: self.assertIsNotNone(txn) for key, value in expected['transaction'].items(): self.assertEquals(value, txn[key])
def __init__(self, start_session, end_session, trading_calendar, capital_base=DEFAULT_CAPITAL_BASE, emission_rate='daily', data_frequency='daily', arena='backtest'): assert type(start_session) == pd.Timestamp assert type(end_session) == pd.Timestamp assert trading_calendar is not None, \ "Must pass in trading calendar!" assert start_session <= end_session, \ "Period start falls after period end." assert start_session <= trading_calendar.last_trading_session, \ "Period start falls after the last known trading day." assert end_session >= trading_calendar.first_trading_session, \ "Period end falls before the first known trading day." # chop off any minutes or hours on the given start and end dates, # as we only support session labels here (and we represent session # labels as midnight UTC). self._start_session = normalize_date(start_session) self._end_session = normalize_date(end_session) self._capital_base = capital_base self._emission_rate = emission_rate self._data_frequency = data_frequency # copied to algorithm's environment for runtime access self._arena = arena self._trading_calendar = trading_calendar if not trading_calendar.is_session(self._start_session): # if the start date is not a valid session in this calendar, # push it forward to the first valid session self._start_session = trading_calendar.minute_to_session_label( self._start_session) if not trading_calendar.is_session(self._end_session): # if the end date is not a valid session in this calendar, # pull it backward to the last valid session before the given # end date. self._end_session = trading_calendar.minute_to_session_label( self._end_session, direction="previous") self._first_open = trading_calendar.open_and_close_for_session( self._start_session)[0] self._last_close = trading_calendar.open_and_close_for_session( self._end_session)[1]
def create_stock_dividend(sid, payment_sid, ratio, declared_date, ex_date, pay_date): return Event({ 'sid': sid, 'payment_sid': payment_sid, 'ratio': ratio, 'net_amount': None, 'gross_amount': None, 'dt': normalize_date(declared_date), 'ex_date': normalize_date(ex_date), 'pay_date': normalize_date(pay_date), 'type': DATASOURCE_TYPE.DIVIDEND, 'source_id': 'MockDividendSource' })
def handle_minute_close(self, dt, data_portal): """ Handles the close of the given minute in minute emission. Parameters __________ dt : Timestamp The minute that is ending Returns _______ A minute perf packet. """ self.position_tracker.sync_last_sale_prices(dt, False, data_portal) self.update_performance() todays_date = normalize_date(dt) account = self.get_account(False) bench_returns = self.all_benchmark_returns.loc[todays_date:dt] # cumulative returns bench_since_open = (1. + bench_returns).prod() - 1 self.cumulative_risk_metrics.update(todays_date, self.todays_performance.returns, bench_since_open, account.leverage) minute_packet = self.to_dict(emission_type='minute') return minute_packet
def handle_data(context, data): today = normalize_date(get_datetime()) results = pipeline_output('test') expect_over_300 = { AAPL: today < self.AAPL_split_date, MSFT: False, BRK_A: True, } for asset in assets: should_pass_filter = expect_over_300[asset] if set_screen and not should_pass_filter: self.assertNotIn(asset, results.index) continue asset_results = results.loc[asset] self.assertEqual(asset_results['filter'], should_pass_filter) for length in vwaps: computed = results.loc[asset, vwap_key(length)] expected = vwaps[length][asset].loc[today] # Only having two places of precision here is a bit # unfortunate. assert_almost_equal(computed, expected, decimal=2)
def transform(self): """ Main generator work loop. """ algo = self.algo emission_rate = algo.perf_tracker.emission_rate def every_bar(dt_to_use, current_data=self.current_data, handle_data=algo.event_manager.handle_data): # called every tick (minute or day). algo.on_dt_changed(dt_to_use) for capital_change in calculate_minute_capital_changes(dt_to_use): yield capital_change self.simulation_dt = dt_to_use blotter = algo.blotter perf_tracker = algo.perf_tracker # handle any transactions and commissions coming out new orders # placed in the last bar new_transactions, new_commissions, closed_orders = \ blotter.get_transactions(current_data) blotter.prune_orders(closed_orders) for transaction in new_transactions: perf_tracker.process_transaction(transaction) # since this order was modified, record it order = blotter.orders[transaction.order_id] perf_tracker.process_order(order) if new_commissions: for commission in new_commissions: perf_tracker.process_commission(commission) handle_data(algo, current_data, dt_to_use) # grab any new orders from the blotter, then clear the list. # this includes cancelled orders. new_orders = blotter.new_orders blotter.new_orders = [] # if we have any new orders, record them so that we know # in what perf period they were placed. if new_orders: for new_order in new_orders: perf_tracker.process_order(new_order) algo.portfolio_needs_update = True algo.account_needs_update = True algo.performance_needs_update = True def once_a_day(midnight_dt, current_data=self.current_data, data_portal=self.data_portal): perf_tracker = algo.perf_tracker # Get the positions before updating the date so that prices are # fetched for trading close instead of midnight positions = algo.perf_tracker.position_tracker.positions position_assets = algo.asset_finder.retrieve_all(positions) # set all the timestamps self.simulation_dt = midnight_dt algo.on_dt_changed(midnight_dt) # process any capital changes that came overnight for capital_change in algo.calculate_capital_changes( midnight_dt, emission_rate=emission_rate, is_interday=True): yield capital_change # we want to wait until the clock rolls over to the next day # before cleaning up expired assets. self._cleanup_expired_assets(midnight_dt, position_assets) # handle any splits that impact any positions or any open orders. assets_we_care_about = \ viewkeys(perf_tracker.position_tracker.positions) | \ viewkeys(algo.blotter.open_orders) if assets_we_care_about: splits = data_portal.get_splits(assets_we_care_about, midnight_dt) if splits: algo.blotter.process_splits(splits) perf_tracker.position_tracker.handle_splits(splits) def handle_benchmark(date, benchmark_source=self.benchmark_source): algo.perf_tracker.all_benchmark_returns[date] = \ benchmark_source.get_value(date) def on_exit(): # Remove references to algo, data portal, et al to break cycles # and ensure deterministic cleanup of these objects when the # simulation finishes. self.algo = None self.benchmark_source = self.current_data = self.data_portal = None with ExitStack() as stack: stack.callback(on_exit) stack.enter_context(self.processor) stack.enter_context(ZiplineAPI(self.algo)) if algo.data_frequency == 'minute': def execute_order_cancellation_policy(): algo.blotter.execute_cancel_policy(SESSION_END) def calculate_minute_capital_changes(dt): # process any capital changes that came between the last # and current minutes return algo.calculate_capital_changes( dt, emission_rate=emission_rate, is_interday=False) else: def execute_order_cancellation_policy(): pass def calculate_minute_capital_changes(dt): return [] for dt, action in self.clock: if action == BAR: for capital_change_packet in every_bar(dt): yield capital_change_packet elif action == SESSION_START: for capital_change_packet in once_a_day(dt): yield capital_change_packet elif action == SESSION_END: # End of the session. if emission_rate == 'daily': handle_benchmark(normalize_date(dt)) execute_order_cancellation_policy() yield self._get_daily_message(dt, algo, algo.perf_tracker) elif action == BEFORE_TRADING_START_BAR: self.simulation_dt = dt algo.on_dt_changed(dt) algo.before_trading_start(self.current_data) elif action == MINUTE_END: handle_benchmark(dt) minute_msg = \ self._get_minute_message(dt, algo, algo.perf_tracker) yield minute_msg risk_message = algo.perf_tracker.handle_simulation_end() yield risk_message
def _run(handle_data, initialize, before_trading_start, analyze, algofile, algotext, defines, data_frequency, capital_base, data, bundle, bundle_timestamp, start, end, output, print_algo, local_namespace, environ, live, exchange, algo_namespace, base_currency, live_graph, analyze_live, simulate_orders, auth_aliases, stats_output): """Run a backtest for the given algorithm. This is shared between the cli and :func:`catalyst.run_algo`. """ # TODO: refactor for more granularity if algotext is not None: if local_namespace: ip = get_ipython() # noqa namespace = ip.user_ns else: namespace = {} for assign in defines: try: name, value = assign.split('=', 2) except ValueError: raise ValueError( 'invalid define %r, should be of the form name=value' % assign, ) try: # evaluate in the same namespace so names may refer to # eachother namespace[name] = eval(value, namespace) except Exception as e: raise ValueError( 'failed to execute definition for name %r: %s' % (name, e), ) elif defines: raise _RunAlgoError( 'cannot pass define without `algotext`', "cannot pass '-D' / '--define' without '-t' / '--algotext'", ) else: namespace = {} if algofile is not None: algotext = algofile.read() if print_algo: if PYGMENTS: highlight( algotext, PythonLexer(), TerminalFormatter(), outfile=sys.stdout, ) else: click.echo(algotext) log.warn( 'Catalyst is currently in ALPHA. It is going through rapid ' 'development and it is subject to errors. Please use carefully. ' 'We encourage you to report any issue on GitHub: ' 'https://github.com/enigmampc/catalyst/issues' ) log.info('Catalyst version {}'.format(catalyst.__version__)) sleep(3) if live: if simulate_orders: mode = 'paper-trading' else: mode = 'live-trading' else: mode = 'backtest' log.info('running algo in {mode} mode'.format(mode=mode)) exchange_name = exchange if exchange_name is None: raise ValueError('Please specify at least one exchange.') if isinstance(auth_aliases, string_types): aliases = auth_aliases.split(',') if len(aliases) < 2 or len(aliases) % 2 != 0: raise ValueError( 'the `auth_aliases` parameter must contain an even list ' 'of comma-delimited values. For example, ' '"binance,auth2" or "binance,auth2,bittrex,auth2".' ) auth_aliases = dict(zip(aliases[::2], aliases[1::2])) exchange_list = [x.strip().lower() for x in exchange.split(',')] exchanges = dict() for name in exchange_list: if auth_aliases is not None and name in auth_aliases: auth_alias = auth_aliases[name] else: auth_alias = None exchanges[name] = get_exchange( exchange_name=name, base_currency=base_currency, must_authenticate=(live and not simulate_orders), skip_init=True, auth_alias=auth_alias, ) open_calendar = get_calendar('OPEN') env = TradingEnvironment( load=partial( load_crypto_market_data, environ=environ, start_dt=start, end_dt=end ), environ=environ, exchange_tz='UTC', asset_db_path=None # We don't need an asset db, we have exchanges ) env.asset_finder = ExchangeAssetFinder(exchanges=exchanges) def choose_loader(column): bound_cols = TradingPairPricing.columns if column in bound_cols: return ExchangePricingLoader(data_frequency) raise ValueError( "No PipelineLoader registered for column %s." % column ) if live: start = pd.Timestamp.utcnow() # TODO: fix the end data. if end is None: end = start + timedelta(hours=8760) data = DataPortalExchangeLive( exchanges=exchanges, asset_finder=env.asset_finder, trading_calendar=open_calendar, first_trading_day=pd.to_datetime('today', utc=True) ) sim_params = create_simulation_parameters( start=start, end=end, capital_base=capital_base, emission_rate='minute', data_frequency='minute' ) # TODO: use the constructor instead sim_params._arena = 'live' algorithm_class = partial( ExchangeTradingAlgorithmLive, exchanges=exchanges, algo_namespace=algo_namespace, live_graph=live_graph, simulate_orders=simulate_orders, stats_output=stats_output, analyze_live=analyze_live, end=end, ) elif exchanges: # Removed the existing Poloniex fork to keep things simple # We can add back the complexity if required. # I don't think that we should have arbitrary price data bundles # Instead, we should center this data around exchanges. # We still need to support bundles for other misc data, but we # can handle this later. if start != normalize_date(start) or \ end != normalize_date(end): # todo: add to Sim_Params the option to start & end at specific times log.warn( "Catalyst currently starts and ends on the start and " "end of the dates specified, respectively. We hope to " "Modify this and support specific times in a future release." ) data = DataPortalExchangeBacktest( exchange_names=[exchange_name for exchange_name in exchanges], asset_finder=None, trading_calendar=open_calendar, first_trading_day=start, last_available_session=end ) sim_params = create_simulation_parameters( start=start, end=end, capital_base=capital_base, data_frequency=data_frequency, emission_rate=data_frequency, ) algorithm_class = partial( ExchangeTradingAlgorithmBacktest, exchanges=exchanges ) elif bundle is not None: bundle_data = load( bundle, environ, bundle_timestamp, ) prefix, connstr = re.split( r'sqlite:///', str(bundle_data.asset_finder.engine.url), maxsplit=1, ) if prefix: raise ValueError( "invalid url %r, must begin with 'sqlite:///'" % str(bundle_data.asset_finder.engine.url), ) env = TradingEnvironment(asset_db_path=connstr, environ=environ) first_trading_day = \ bundle_data.equity_minute_bar_reader.first_trading_day data = DataPortal( env.asset_finder, open_calendar, first_trading_day=first_trading_day, equity_minute_reader=bundle_data.equity_minute_bar_reader, equity_daily_reader=bundle_data.equity_daily_bar_reader, adjustment_reader=bundle_data.adjustment_reader, ) perf = algorithm_class( namespace=namespace, env=env, get_pipeline_loader=choose_loader, sim_params=sim_params, **{ 'initialize': initialize, 'handle_data': handle_data, 'before_trading_start': before_trading_start, 'analyze': analyze, } if algotext is None else { 'algo_filename': getattr(algofile, 'name', '<algorithm>'), 'script': algotext, } ).run( data, overwrite_sim_params=False, ) if output == '-': click.echo(str(perf)) elif output != os.devnull: # make the catalyst magic not write any data perf.to_pickle(output) return perf
def __init__(self, sim_params, treasury_curves, trading_calendar, create_first_day_stats=False): self.treasury_curves = treasury_curves self.trading_calendar = trading_calendar self.start_session = sim_params.start_session self.end_session = sim_params.end_session self.sessions = trading_calendar.sessions_in_range( self.start_session, self.end_session) # Hold on to the trading day before the start, # used for index of the zero return value when forcing returns # on the first day. self.day_before_start = self.start_session - self.sessions.freq last_day = normalize_date(sim_params.end_session) if last_day not in self.sessions: last_day = pd.tseries.index.DatetimeIndex([last_day]) self.sessions = self.sessions.append(last_day) self.sim_params = sim_params self.create_first_day_stats = create_first_day_stats cont_index = self.sessions self.cont_index = cont_index self.cont_len = len(self.cont_index) empty_cont = np.full(self.cont_len, np.nan) self.algorithm_returns_cont = empty_cont.copy() self.benchmark_returns_cont = empty_cont.copy() self.algorithm_cumulative_leverages_cont = empty_cont.copy() self.mean_returns_cont = empty_cont.copy() self.annualized_mean_returns_cont = empty_cont.copy() self.mean_benchmark_returns_cont = empty_cont.copy() self.annualized_mean_benchmark_returns_cont = empty_cont.copy() # The returns at a given time are read and reset from the respective # returns container. self.algorithm_returns = None self.benchmark_returns = None self.mean_returns = None self.annualized_mean_returns = None self.mean_benchmark_returns = None self.annualized_mean_benchmark_returns = None self.algorithm_cumulative_returns = empty_cont.copy() self.benchmark_cumulative_returns = empty_cont.copy() self.algorithm_cumulative_leverages = empty_cont.copy() self.excess_returns = empty_cont.copy() self.latest_dt_loc = 0 self.latest_dt = cont_index[0] self.benchmark_volatility = empty_cont.copy() self.algorithm_volatility = empty_cont.copy() self.beta = empty_cont.copy() self.alpha = empty_cont.copy() self.sharpe = empty_cont.copy() self.downside_risk = empty_cont.copy() self.sortino = empty_cont.copy() self.information = empty_cont.copy() self.drawdowns = empty_cont.copy() self.max_drawdowns = empty_cont.copy() self.max_drawdown = 0 self.max_leverages = empty_cont.copy() self.max_leverage = 0 self.current_max = -np.inf self.daily_treasury = pd.Series(index=self.sessions) self.treasury_period_return = np.nan self.num_trading_days = 0
def _get_adjustments_in_range(self, asset, dts, field): """ Get the Float64Multiply objects to pass to an AdjustedArrayWindow. For the use of AdjustedArrayWindow in the loader, which looks back from current simulation time back to a window of data the dictionary is structured with: - the key into the dictionary for adjustments is the location of the day from which the window is being viewed. - the start of all multiply objects is always 0 (in each window all adjustments are overlapping) - the end of the multiply object is the location before the calendar location of the adjustment action, making all days before the event adjusted. Parameters ---------- asset : Asset The assets for which to get adjustments. dts : iterable of datetime64-like The dts for which adjustment data is needed. field : str OHLCV field for which to get the adjustments. Returns ------- out : dict[loc -> Float64Multiply] The adjustments as a dict of loc -> Float64Multiply """ sid = int(asset) start = normalize_date(dts[0]) end = normalize_date(dts[-1]) adjs = {} if field != 'volume': mergers = self._adjustments_reader.get_adjustments_for_sid( 'mergers', sid) for m in mergers: dt = m[0] if start < dt <= end: end_loc = dts.searchsorted(dt) adj_loc = end_loc mult = Float64Multiply(0, end_loc - 1, 0, 0, m[1]) try: adjs[adj_loc].append(mult) except KeyError: adjs[adj_loc] = [mult] divs = self._adjustments_reader.get_adjustments_for_sid( 'dividends', sid) for d in divs: dt = d[0] if start < dt <= end: end_loc = dts.searchsorted(dt) adj_loc = end_loc mult = Float64Multiply(0, end_loc - 1, 0, 0, d[1]) try: adjs[adj_loc].append(mult) except KeyError: adjs[adj_loc] = [mult] splits = self._adjustments_reader.get_adjustments_for_sid( 'splits', sid) for s in splits: dt = s[0] if start < dt <= end: if field == 'volume': ratio = 1.0 / s[1] else: ratio = s[1] end_loc = dts.searchsorted(dt) adj_loc = end_loc mult = Float64Multiply(0, end_loc - 1, 0, 0, ratio) try: adjs[adj_loc].append(mult) except KeyError: adjs[adj_loc] = [mult] return adjs