diff --git a/catalyst/examples/mean_reversion_simple.py b/catalyst/examples/mean_reversion_simple.py index 4508c8e5..0e1c98d7 100644 --- a/catalyst/examples/mean_reversion_simple.py +++ b/catalyst/examples/mean_reversion_simple.py @@ -34,7 +34,7 @@ def initialize(context): # parameters or values you're going to use. # In our example, we're looking at Neo in Ether. - context.market = symbol('rdn_eth') + context.market = symbol('neo_eth') context.base_price = None context.current_day = None @@ -240,7 +240,7 @@ def analyze(context=None, perf=None): if __name__ == '__main__': # The execution mode: backtest or live - MODE = 'live' + MODE = 'backtest' if MODE == 'backtest': folder = os.path.join( @@ -259,7 +259,7 @@ if __name__ == '__main__': initialize=initialize, handle_data=handle_data, analyze=analyze, - exchange_name='bittrex', + exchange_name='bitfinex', algo_namespace=NAMESPACE, base_currency='eth', start=pd.to_datetime('2017-10-01', utc=True), diff --git a/catalyst/examples/simple_loop.py b/catalyst/examples/simple_loop.py index b0dbd929..9aa3b9f1 100644 --- a/catalyst/examples/simple_loop.py +++ b/catalyst/examples/simple_loop.py @@ -109,9 +109,9 @@ def analyze(context, perf): run_algorithm( capital_base=250, - start=pd.to_datetime('2017-11-1 0:00', utc=True), + start=pd.to_datetime('2017-11-9 0:00', utc=True), end=pd.to_datetime('2017-11-10 23:59', utc=True), - data_frequency='daily', + data_frequency='minute', initialize=initialize, handle_data=handle_data, analyze=analyze, diff --git a/catalyst/exchange/exchange_algorithm.py b/catalyst/exchange/exchange_algorithm.py index ff7dc94a..b98357d6 100644 --- a/catalyst/exchange/exchange_algorithm.py +++ b/catalyst/exchange/exchange_algorithm.py @@ -13,7 +13,6 @@ import pickle import signal import sys -from collections import deque from datetime import timedelta from os import listdir from os.path import isfile, join @@ -631,50 +630,72 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase): self.validate_account_controls() try: - # Since the clock runs 24/7, I trying to disable the daily - # Performance tracker and keep only minute and cumulative - self.perf_tracker.update_performance() - - frame_stats = self.prepare_period_stats( - data.current_dt, data.current_dt + timedelta(minutes=1)) - - # Saving the last hour in memory - self.frame_stats.append(frame_stats) - - self.add_pnl_stats(frame_stats) - if self.recorded_vars: - self.add_custom_signals_stats(frame_stats) - recorded_cols = list(self.recorded_vars.keys()) - else: - recorded_cols = None - - self.add_exposure_stats(frame_stats) - - # print_df = pd.DataFrame(list(self.frame_stats)) - log.info( - 'statistics for the last {stats_minutes} minutes:\n' - '{stats}'.format( - stats_minutes=self.stats_minutes, - stats=get_pretty_stats( - stats=self.frame_stats, - recorded_cols=recorded_cols, - num_rows=self.stats_minutes - ) - )) - - daily_stats = self.prepare_period_stats( - start_dt=today, - end_dt=pd.Timestamp.utcnow() - ) - save_algo_object( - algo_name=self.algo_namespace, - key=today.strftime('%Y-%m-%d'), - obj=daily_stats, - rel_path='daily_perf' - ) + self._save_stats_csv(self._process_stats(data)) except Exception as e: log.warn('unable to calculate performance: {}'.format(e)) + # TODO: pickle does not seem to work in python 3 + try: + save_algo_object( + algo_name=self.algo_namespace, + key='perf_tracker', + obj=self.perf_tracker + ) + except Exception as e: + log.warn('unable to save minute perfs to disk: {}'.format(e)) + + self.current_day = data.current_dt.floor('1D') + + def _process_stats(self, data): + today = data.current_dt.floor('1D') + + # Since the clock runs 24/7, I trying to disable the daily + # Performance tracker and keep only minute and cumulative + self.perf_tracker.update_performance() + + frame_stats = self.prepare_period_stats( + data.current_dt, data.current_dt + timedelta(minutes=1)) + + # Saving the last hour in memory + self.frame_stats.append(frame_stats) + + self.add_pnl_stats(frame_stats) + if self.recorded_vars: + self.add_custom_signals_stats(frame_stats) + recorded_cols = list(self.recorded_vars.keys()) + + else: + recorded_cols = None + + self.add_exposure_stats(frame_stats) + + log.info( + 'statistics for the last {stats_minutes} minutes:\n' + '{stats}'.format( + stats_minutes=self.stats_minutes, + stats=get_pretty_stats( + stats=self.frame_stats, + recorded_cols=recorded_cols, + num_rows=self.stats_minutes + ) + )) + + # Saving the daily stats in a format usable for performance + # analysis. + daily_stats = self.prepare_period_stats( + start_dt=today, + end_dt=data.current_dt + ) + save_algo_object( + algo_name=self.algo_namespace, + key=today.strftime('%Y-%m-%d'), + obj=daily_stats, + rel_path='daily_perf' + ) + + return recorded_cols + + def _save_stats_csv(self, recorded_cols): # Writing the stats output csv_bytes = None try: @@ -683,7 +704,6 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase): algo_namespace=self.algo_namespace, recorded_cols=recorded_cols, ) - except Exception as e: log.warn('unable save stats locally: {}'.format(e)) @@ -697,27 +717,13 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase): recorded_cols=recorded_cols, bytes_to_write=csv_bytes ) - else: raise ValueError( 'Only S3 stats output is supported for now.' ) - except Exception as e: log.warn('unable save stats externally: {}'.format(e)) - # TODO: pickle does not seem to work in python 3 - try: - save_algo_object( - algo_name=self.algo_namespace, - key='perf_tracker', - obj=self.perf_tracker - ) - except Exception as e: - log.warn('unable to save minute perfs to disk: {}'.format(e)) - - self.current_day = data.current_dt.floor('1D') - @api_method def batch_market_order(self, share_counts): raise NotImplementedError() diff --git a/catalyst/exchange/exchange_errors.py b/catalyst/exchange/exchange_errors.py index 4f1acd9c..7744aa44 100644 --- a/catalyst/exchange/exchange_errors.py +++ b/catalyst/exchange/exchange_errors.py @@ -244,7 +244,7 @@ class NoDataAvailableOnExchange(ZiplineError): 'exchange {exchange} ' 'in `{data_frequency}` frequency at this time. ' 'Check `http://enigma.co/catalyst/status` for market coverage.' - ).strip() + ).strip() class NoValueForField(ZiplineError): @@ -255,3 +255,11 @@ class OrderTypeNotSupported(ZiplineError): msg = ( 'Order type `{order_type}` not currencly supported by Catalyst. ' 'Please use `limit` or `market` orders only.').strip() + + +class NotEnoughCapitalError(ZiplineError): + msg = ( + 'Not enough capital on exchange {exchange} for trading. Each ' + 'exchange should contain at least as much {base_currency} ' + 'as the specified `capital_base`. The current balance {balance} is ' + 'lower than the `capital_base`: {capital_base}').strip() diff --git a/catalyst/exchange/stats_utils.py b/catalyst/exchange/stats_utils.py index 737ad608..6c5d49bd 100644 --- a/catalyst/exchange/stats_utils.py +++ b/catalyst/exchange/stats_utils.py @@ -192,20 +192,21 @@ def prepare_stats(stats, recorded_cols=list()): assets = [p['sid'] for p in row_data['positions']] asset_values = dict() - for column in recorded_cols[:]: - value = row_data[column] - if type(value) is dict: - for asset in value: - if not isinstance(asset, TradingPair): - break + if recorded_cols is not None: + for column in recorded_cols[:]: + value = row_data[column] + if type(value) is dict: + for asset in value: + if not isinstance(asset, TradingPair): + break - if asset not in assets: - assets.append(asset) + if asset not in assets: + assets.append(asset) - if asset not in asset_values: - asset_values[asset] = dict() + if asset not in asset_values: + asset_values[asset] = dict() - asset_values[asset][column] = value[asset] + asset_values[asset][column] = value[asset] if len(assets) == 1: row = stats[row_index] @@ -231,8 +232,8 @@ def prepare_stats(stats, recorded_cols=list()): ] # Removing the asset specific entries - recorded_cols = [x for x in recorded_cols if x not in asset_cols] if recorded_cols is not None: + recorded_cols = [x for x in recorded_cols if x not in asset_cols] for column in recorded_cols: index_cols.append(column) @@ -256,13 +257,24 @@ def get_pretty_stats(stats, recorded_cols=None, num_rows=10): Parameters ---------- stats: list[Object] + An array of statistics for the period. + num_rows: int + The number of rows to display on the screen. Returns ------- str """ + if isinstance(stats, pd.DataFrame): + # df = stats + # columns = [ + # 'period_close', 'starting_cash', 'ending_cash', 'portfolio_value', + # 'pnl', 'long_exposure', 'short_exposure', 'orders', 'transactions', + # ] + stats = stats.T.to_dict().values() + # else: df, columns = prepare_stats(stats, recorded_cols=recorded_cols) pd.set_option('display.expand_frame_repr', False) diff --git a/catalyst/utils/run_algo.py b/catalyst/utils/run_algo.py index 3c62ea2d..a3442ec9 100644 --- a/catalyst/utils/run_algo.py +++ b/catalyst/utils/run_algo.py @@ -40,7 +40,7 @@ from catalyst.exchange.exchange_data_portal import DataPortalExchangeLive, \ from catalyst.exchange.asset_finder_exchange import AssetFinderExchange from catalyst.exchange.exchange_errors import ( ExchangeRequestError, ExchangeRequestErrorTooManyAttempts, - BaseCurrencyNotFoundError) + BaseCurrencyNotFoundError, NotEnoughCapitalError) from catalyst.constants import LOG_LEVEL @@ -227,28 +227,25 @@ def _run(handle_data, ) ) - if capital_base is not None \ - and capital_base < base_currency_available: - log.info( - 'using capital base limit: {} {}'.format( - capital_base, base_currency - ) - ) - amount = capital_base - else: - amount = base_currency_available - - return amount + return base_currency_available else: raise BaseCurrencyNotFoundError( base_currency=base_currency, exchange=exchange_name ) - combined_capital_base = 0 - for exchange_name in exchanges: - exchange = exchanges[exchange_name] - combined_capital_base += fetch_capital_base(exchange) + if not simulate_orders: + for exchange_name in exchanges: + exchange = exchanges[exchange_name] + balance = fetch_capital_base(exchange) + + if balance < capital_base: + raise NotEnoughCapitalError( + exchange=exchange_name, + base_currency=base_currency, + balance=balance, + capital_base=capital_base, + ) sim_params = create_simulation_parameters( start=start, @@ -505,6 +502,14 @@ def run_algorithm(initialize, default_extension, extensions, strict_extensions, environ ) + if capital_base is None: + raise ValueError( + 'Please specify a `capital_base` parameter which is the maximum ' + 'amount of base currency available for trading. For example, ' + 'if the `capital_base` is 5ETH, the ' + '`order_target_percent(asset, 1)` command will order 5ETH worth ' + 'of the specified asset.' + ) # I'm not sure that we need this since the modified DataPortal # does not require extensions to be explicitly loaded.