diff --git a/catalyst/examples/momemtum.py b/catalyst/examples/mean-reversion.py similarity index 51% rename from catalyst/examples/momemtum.py rename to catalyst/examples/mean-reversion.py index 4e2c35fe..967b78c1 100644 --- a/catalyst/examples/momemtum.py +++ b/catalyst/examples/mean-reversion.py @@ -17,6 +17,9 @@ from catalyst.api import symbol, record, order_target_percent, \ # In this example, Catalyst will create the `.catalyst/data/live_algos` # directory. If we stop and start the algorithm, Catalyst will resume its # state using the files included in the folder. +from catalyst.exchange.stats_utils import crossunder, get_pretty_stats, \ + extract_transactions + algo_namespace = 'momentum' log = Logger(algo_namespace) @@ -27,7 +30,7 @@ def initialize(context): # trading pairs) you want to backtest. You'll also want to define any # parameters or values you're going to use. - # In our example, we're looking at Ether in Bitcoin. + # In our example, we're looking at Ether in USD Tether. context.eth_btc = symbol('eth_usdt') context.max_amount = 0.01 context.base_price = None @@ -42,7 +45,8 @@ def handle_data(context, data): # We flag the first period of each day. # Since cryptocurrencies trade 24/7 the `before_trading_starts` handle - # would only execute once. + # would only execute once. This method works with minute and daily + # frequencies. today = data.current_dt.floor('1D') if today != context.current_day: context.traded_today = False @@ -50,43 +54,67 @@ def handle_data(context, data): # We're computing the volume-weighted-average-price of the security # defined above, in the context.eth_btc variable. For this example, we're - # using three bars on the daily chart. + # using three bars on the 15 min bars. + + # The frequency attribute determine the bar size. We use this convention + # for the frequency alias: + # http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases prices = data.history( context.eth_btc, fields='close', - bar_count=100, - frequency='30T' - ) - # Use TA-Lib to calculate MACD data using calibrated settings - macd_raw, signal, macd_hist = talib.MACD( - prices.values, fastperiod=30, slowperiod=40, signalperiod=45 + bar_count=220, + frequency='15T' ) + # Ta-lib calculates various technical indicator based on price and + # volume arrays. + + # In this example, we are comp + rsi = talib.RSI(prices.values, timeperiod=4) + sma200 = talib.SMA(prices.values, timeperiod=200) + # We need a variable for the current price of the security to compare to - # the average. + # the average. Since we are requesting two fields, data.current() + # returns a DataFrame with current = data.current(context.eth_btc, fields=['close', 'volume']) price = current['close'] log.info( - '{}: price: {}, macd: {}'.format(data.current_dt, price, macd_raw[-1]) + '{}: price: {}, rsi: {}, sma: {}'.format( + data.current_dt, price, rsi[-1], sma200[-1] + ) ) # If base_price is not set, we use the current value. This is the # price at the first bar which we reference to calculate price_change. if context.base_price is None: context.base_price = price - price_change = (price - context.base_price) / context.base_price + price_change = (price - context.base_price) / context.base_price + cash = context.portfolio.cash + + # Now that we've collected all current data for this frame, we use + # the record() method to save it. This data will be available as + # a parameter of the analyze() function for further analysis. record( price=price, volume=current['volume'], - macd=macd_raw[-1], - signal=signal[-1], + sma200=sma200[-1], price_change=price_change, + rsi=rsi[-1], + cash=cash ) + # We are trying to avoid over-trading by limiting our trades to + # one per day. + if context.traded_today: + log.info('skipping because we\'ve already trader today') + return + + # Since we are using limit orders, some orders may not execute immediately + # we wait until all orders are executed before considering more trades. orders = get_open_orders(context.eth_btc) if len(orders) > 0: - log.info('skipping bar until all open orders execute') + log.info('skipping until all open orders execute') return # Another powerful built-in feature of the Catalyst backtester is the @@ -95,18 +123,20 @@ def handle_data(context, data): # how long or short our position is at this minute. pos_amount = context.portfolio.positions[context.eth_btc].amount - if macd_hist[-1] > 0 and data.can_trade(context.eth_btc) \ - and pos_amount == 0 and not context.traded_today: - order_target_percent(context.eth_btc, 0.75) + # Determining the entry and exit signals based on RSI and SMA + if rsi[-1] <= 30 and data.can_trade(context.eth_btc) \ + and pos_amount == 0: + # and price > sma200[-1] and pos_amount == 0: + order_target_percent(context.eth_btc, 1) context.traded_today = True - elif macd_hist[-1] < 0 and data.can_trade(context.eth_btc) \ - and pos_amount > 0 and not context.traded_today: + elif (rsi[-1] >= 90 or crossunder(prices, sma200)) \ + and data.can_trade(context.eth_btc) and pos_amount > 0: order_target_percent(context.eth_btc, 0) context.traded_today = True -def analyze(context=None, results=None): +def analyze(context=None, perf=None): import matplotlib.pyplot as plt # The base currency of the algo exchange @@ -114,77 +144,70 @@ def analyze(context=None, results=None): # Plot the portfolio value over time. ax1 = plt.subplot(611) - results.loc[:, 'portfolio_value'].plot(ax=ax1) + perf.loc[:, 'portfolio_value'].plot(ax=ax1) ax1.set_ylabel('Portfolio Value ({})'.format(base_currency)) # Plot the price increase or decrease over time. ax2 = plt.subplot(612, sharex=ax1) - results.loc[:, 'price'].plot(ax=ax2) + perf.loc[:, 'price'].plot(ax=ax2, label='Price') + perf.loc[:, 'sma200'].plot(ax=ax2, label='SMA200') + ax2.set_ylabel('{asset} ({base})'.format( asset=context.eth_btc.symbol, base=base_currency )) - # Compute indexes for buy and sell transactions - trans_list = results.transactions.values - all_trans = [t for sublist in trans_list for t in sublist] - all_trans.sort(key=lambda t: t['dt']) - - buys = results.loc[[t['dt'] for t in all_trans if t['amount'] > 0], :] - sells = results.loc[[t['dt'] for t in all_trans if t['amount'] < 0], :] - + transaction_df = extract_transactions(perf) + buy_df = transaction_df[transaction_df['amount'] > 0] + sell_df = transaction_df[transaction_df['amount'] < 0] ax2.scatter( - buys.index, - results.loc[buys.index, 'price'], + buy_df.index, + perf.loc[buy_df.index, 'price'], marker='^', s=100, c='green', + label='' ) ax2.scatter( - sells.index, - results.loc[sells.index, 'price'], + sell_df.index, + perf.loc[sell_df.index, 'price'], marker='v', s=100, c='red', + label='' ) ax4 = plt.subplot(613, sharex=ax1) - results.loc[:, ['starting_cash', 'cash']].plot(ax=ax4) - ax4.set_ylabel('Base Currency ({})'.format(base_currency)) + perf.loc[:, 'cash'].plot( + ax=ax4, label='Base Currency ({})'.format(base_currency) + ) + ax4.set_ylabel('Cash ({})'.format(base_currency)) - results['algorithm'] = results.loc[:, 'algorithm_period_return'] + perf['algorithm'] = perf.loc[:, 'algorithm_period_return'] ax5 = plt.subplot(614, sharex=ax1) - results.loc[:, ['algorithm', 'price_change']].plot(ax=ax5) + perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5) ax5.set_ylabel('Percent Change') ax6 = plt.subplot(615, sharex=ax1) - results.loc[:, 'macd'].plot(ax=ax6, label='macd') - + perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI') + ax6.axhline(70, color='darkgoldenrod') + ax6.axhline(30, color='darkgoldenrod') ax6.scatter( - buys.index, - results.loc[buys.index, 'macd'], + buy_df.index, + perf.loc[buy_df.index, 'rsi'], marker='^', s=100, c='green', label='' ) ax6.scatter( - sells.index, - results.loc[sells.index, 'macd'], + sell_df.index, + perf.loc[sell_df.index, 'rsi'], marker='v', s=100, c='red', label='' ) - # handles, labels = plt.gca().get_legend_handles_labels() - # i = 1 - # while i < len(labels): - # if labels[i] in labels[:i]: - # del (labels[i]) - # del (handles[i]) - # else: - # i += 1 - plt.legend(loc=3) # Show the plot. @@ -193,16 +216,33 @@ def analyze(context=None, results=None): pass -# Backtest -run_algorithm( - capital_base=1, - data_frequency='minute', - initialize=initialize, - handle_data=handle_data, - analyze=analyze, - exchange_name='poloniex', - algo_namespace=algo_namespace, - base_currency='usdt', - start=pd.to_datetime('2017-6-1', utc=True), - end=pd.to_datetime('2017-6-7', utc=True), -) +if __name__ == '__main__': + # The execution mode: backtest or live + MODE = 'backtest' + + if MODE == 'backtest': + run_algorithm( + capital_base=1, + data_frequency='minute', + initialize=initialize, + handle_data=handle_data, + analyze=analyze, + exchange_name='poloniex', + algo_namespace=algo_namespace, + base_currency='usdt', + start=pd.to_datetime('2017-7-1', utc=True), + end=pd.to_datetime('2017-9-30', utc=True), + # end=pd.to_datetime('2017-7-7', utc=True), + ) + + elif MODE == 'live': + run_algorithm( + initialize=initialize, + handle_data=handle_data, + analyze=analyze, + exchange_name='poloniex', + live=True, + algo_namespace=algo_namespace, + base_currency='usdt', + live_graph=True + ) diff --git a/catalyst/exchange/exchange_bundle.py b/catalyst/exchange/exchange_bundle.py index ff295575..48d3294f 100644 --- a/catalyst/exchange/exchange_bundle.py +++ b/catalyst/exchange/exchange_bundle.py @@ -761,12 +761,18 @@ class ExchangeBundle: The spot values for the gives assets, field and date. Reads from the exchange data bundle. - :param assets: - :param field: - :param dt: - :param data_frequency: - :param reset_reader: - :return: + Parameters + ---------- + assets: list[TradingPair] + field: str + dt: pd.Timestamp + data_frequency: str + reset_reader: + + Returns + ------- + float + """ values = [] try: diff --git a/catalyst/exchange/stats_utils.py b/catalyst/exchange/stats_utils.py index cf5afc00..3aa5e002 100644 --- a/catalyst/exchange/stats_utils.py +++ b/catalyst/exchange/stats_utils.py @@ -1,3 +1,5 @@ +import numbers + import numpy as np import pandas as pd @@ -44,14 +46,24 @@ def crossunder(source, target): bool """ - if source[-1] is np.nan or source[-2] is np.nan \ - or target[-1] is np.nan or target[-2] is np.nan: - return False + if isinstance(target, numbers.Number): + if source[-1] is np.nan or source[-2] is np.nan \ + or target is np.nan: + return False - if source[-1] < target[-1] and source[-2] > target[-2]: - return True + if source[-1] < target <= source[-2]: + return True + else: + return False else: - return False + if source[-1] is np.nan or source[-2] is np.nan \ + or target[-1] is np.nan or target[-2] is np.nan: + return False + + if source[-1] < target[-1] and source[-2] >= target[-2]: + return True + else: + return False def vwap(df): @@ -161,3 +173,29 @@ def df_to_string(df): pd.set_option('display.max_colwidth', 1000) return df.to_string() + + +def extract_transactions(perf): + """ + Compute indexes for buy and sell transactions + + Parameters + ---------- + perf: DataFrame + The algo performance DataFrame. + + Returns + ------- + DataFrame + A DataFrame of transactions. + + """ + trans_list = perf.transactions.values + all_trans = [t for sublist in trans_list for t in sublist] + all_trans.sort(key=lambda t: t['dt']) + + # transactions = perf.loc[[t['dt'] for t in all_trans], :] + + transactions = pd.DataFrame(all_trans) + transactions.set_index('dt', inplace=True, drop=True) + return transactions