From c2821ab77b7d16bff82c73d0ba0eac4ac9cf4d82 Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Mon, 27 Nov 2017 15:43:57 -0700 Subject: [PATCH 01/11] DOC: added example_algo: Dual Moving Average Crossover --- catalyst/examples/dual_moving_average.py | 153 +++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 catalyst/examples/dual_moving_average.py diff --git a/catalyst/examples/dual_moving_average.py b/catalyst/examples/dual_moving_average.py new file mode 100644 index 00000000..b724ec24 --- /dev/null +++ b/catalyst/examples/dual_moving_average.py @@ -0,0 +1,153 @@ +import numpy as np +import pandas as pd +from logbook import Logger +import matplotlib.pyplot as plt + +from catalyst import run_algorithm +from catalyst.api import (order, record, symbol, order_target_percent, + get_open_orders) +from catalyst.exchange.stats_utils import extract_transactions + +NAMESPACE = 'dual_moving_average' +log = Logger(NAMESPACE) + +def initialize(context): + context.i = 0 + context.asset = symbol('ltc_usd') + context.base_price = None + + +def handle_data(context, data): + # define the windows for the moving averages + short_window = 50 + long_window = 200 + + # Skip as many bars as long_window to properly compute the average + context.i += 1 + if context.i < long_window: + return + + # Compute moving averages calling data.history() for each + # moving average with the appropriate parameters. We choose to use + # minute bars for this simulation -> freq="1m" + # Returns a pandas dataframe. + short_mavg = data.history(context.asset, 'price', + bar_count=short_window, frequency="1m").mean() + long_mavg = data.history(context.asset, 'price', + bar_count=long_window, frequency="1m").mean() + + # Let's keep the price of our asset in a more handy variable + price = data.current(context.asset, 'price') + + # 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 + + # Save values for later inspection + record(price=price, + cash=context.portfolio.cash, + price_change=price_change, + short_mavg=short_mavg, + long_mavg=long_mavg) + + # 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.asset) + if len(orders) > 0: + return + + # Exit if we cannot trade + if not data.can_trade(context.asset): + return + + # We check what's our position on our portfolio and trade accordingly + pos_amount = context.portfolio.positions[context.asset].amount + + # Trading logic + if short_mavg > long_mavg and pos_amount == 0: + # we buy 100% of our portfolio for this asset + order_target_percent(context.asset, 1) + elif short_mavg < long_mavg and pos_amount > 0: + # we sell all our positions for this asset + order_target_percent(context.asset, 0) + + +def analyze(context, perf): + + # Get the base_currency that was passed as a parameter to the simulation + base_currency = context.exchanges.values()[0].base_currency.upper() + + # First subgraph: Plot portfolio value using base_currency + ax1 = plt.subplot(411) + perf.loc[:, ['portfolio_value']].plot(ax=ax1) + ax1.legend_.remove() + ax1.set_ylabel('Portfolio Value\n({})'.format(base_currency)) + start, end = ax1.get_ylim() + ax1.yaxis.set_ticks(np.arange(start, end, (end-start)/5)) + + # Second subgraph: Plot asset price, moving averages and buys/sells + ax2 = plt.subplot(412, sharex=ax1) + perf.loc[:, ['price','short_mavg','long_mavg']].plot(ax=ax2, label='Price') + ax2.legend_.remove() + ax2.set_ylabel('{asset}\n({base})'.format( + asset = context.asset.symbol, + base = base_currency + )) + start, end = ax2.get_ylim() + ax2.yaxis.set_ticks(np.arange(start, end, (end-start)/5)) + + transaction_df = extract_transactions(perf) + if not transaction_df.empty: + buy_df = transaction_df[transaction_df['amount'] > 0] + sell_df = transaction_df[transaction_df['amount'] < 0] + ax2.scatter( + buy_df.index.to_pydatetime(), + perf.loc[buy_df.index, 'price'], + marker='^', + s=100, + c='green', + label='' + ) + ax2.scatter( + sell_df.index.to_pydatetime(), + perf.loc[sell_df.index, 'price'], + marker='v', + s=100, + c='red', + label='' + ) + + # Third subgraph: Compare percentage change between our portfolio + # and the price of the asset + ax3 = plt.subplot(413, sharex=ax1) + perf.loc[:, ['algorithm_period_return', 'price_change']].plot(ax=ax3) + ax3.legend_.remove() + ax3.set_ylabel('Percent Change') + start, end = ax3.get_ylim() + ax3.yaxis.set_ticks(np.arange(start, end, (end-start)/5)) + + # Plot our cash + ax4 = plt.subplot(414, sharex=ax1) + perf.cash.plot(ax=ax4) + ax4.set_ylabel('Cash\n({})'.format(base_currency)) + start, end = ax4.get_ylim() + ax4.yaxis.set_ticks(np.arange(0, end, end/5)) + + plt.show() + + +if __name__ == '__main__': + run_algorithm( + capital_base=1000, + data_frequency='minute', + initialize=initialize, + handle_data=handle_data, + analyze=analyze, + exchange_name='bitfinex', + algo_namespace=NAMESPACE, + base_currency='usd', + start=pd.to_datetime('2017-9-22', utc=True), + end=pd.to_datetime('2017-9-23', utc=True), + ) \ No newline at end of file From d07e0edd88f035593eb13d5ecc847c004f97fe16 Mon Sep 17 00:00:00 2001 From: fredfortier Date: Mon, 27 Nov 2017 17:54:34 -0500 Subject: [PATCH 02/11] DOC: 0.3.9 release notes --- docs/source/releases.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/source/releases.rst b/docs/source/releases.rst index b178ec58..67f32498 100644 --- a/docs/source/releases.rst +++ b/docs/source/releases.rst @@ -2,6 +2,20 @@ Release Notes ============= +Version 0.3.9 +^^^^^^^^^^^^^ +**Release Date**: 2017-11-14 + +Bug Fixes +~~~~~~~~~ + +- Improved cash display in running stats (:issue:`80`) +- Added capital_base parameter to live mode to limit cash (:issue:`79`) +- Fixed sortino warning issues (:issue:`77`) +- Adjusted computation of last candle of data.history (:issue:`71`) +- Added support for csv ingestion (:issue:`65`) + + Version 0.3.8 ^^^^^^^^^^^^^ **Release Date**: 2017-11-14 From a61b22b821a86995b30358437f2bcbb26abf394f Mon Sep 17 00:00:00 2001 From: fredfortier Date: Mon, 27 Nov 2017 17:55:22 -0500 Subject: [PATCH 03/11] DOC: 0.3.9 release notes --- docs/source/releases.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/source/releases.rst b/docs/source/releases.rst index 67f32498..2695c309 100644 --- a/docs/source/releases.rst +++ b/docs/source/releases.rst @@ -9,11 +9,14 @@ Version 0.3.9 Bug Fixes ~~~~~~~~~ -- Improved cash display in running stats (:issue:`80`) -- Added capital_base parameter to live mode to limit cash (:issue:`79`) - Fixed sortino warning issues (:issue:`77`) - Adjusted computation of last candle of data.history (:issue:`71`) + +Build +~~~~~ +- Added capital_base parameter to live mode to limit cash (:issue:`79`) - Added support for csv ingestion (:issue:`65`) +- Improved cash display in running stats (:issue:`80`) Version 0.3.8 From a52e201f866f01eeb7ab50d40531168054acce87 Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Mon, 27 Nov 2017 21:10:17 -0700 Subject: [PATCH 04/11] DOC: improved dual_moving_average.py example algo --- catalyst/examples/dual_moving_average.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/catalyst/examples/dual_moving_average.py b/catalyst/examples/dual_moving_average.py index b724ec24..a73461f1 100644 --- a/catalyst/examples/dual_moving_average.py +++ b/catalyst/examples/dual_moving_average.py @@ -1,10 +1,10 @@ import numpy as np import pandas as pd from logbook import Logger -import matplotlib.pyplot as plt +import matplotlib.pyplot as plt from catalyst import run_algorithm -from catalyst.api import (order, record, symbol, order_target_percent, +from catalyst.api import (order, record, symbol, order_target_percent, get_open_orders) from catalyst.exchange.stats_utils import extract_transactions @@ -18,7 +18,7 @@ def initialize(context): def handle_data(context, data): - # define the windows for the moving averages + # define the windows for the moving averages short_window = 50 long_window = 200 @@ -27,11 +27,11 @@ def handle_data(context, data): if context.i < long_window: return - # Compute moving averages calling data.history() for each + # Compute moving averages calling data.history() for each # moving average with the appropriate parameters. We choose to use # minute bars for this simulation -> freq="1m" # Returns a pandas dataframe. - short_mavg = data.history(context.asset, 'price', + short_mavg = data.history(context.asset, 'price', bar_count=short_window, frequency="1m").mean() long_mavg = data.history(context.asset, 'price', bar_count=long_window, frequency="1m").mean() @@ -79,7 +79,7 @@ def analyze(context, perf): # Get the base_currency that was passed as a parameter to the simulation base_currency = context.exchanges.values()[0].base_currency.upper() - # First subgraph: Plot portfolio value using base_currency + # First chart: Plot portfolio value using base_currency ax1 = plt.subplot(411) perf.loc[:, ['portfolio_value']].plot(ax=ax1) ax1.legend_.remove() @@ -87,7 +87,7 @@ def analyze(context, perf): start, end = ax1.get_ylim() ax1.yaxis.set_ticks(np.arange(start, end, (end-start)/5)) - # Second subgraph: Plot asset price, moving averages and buys/sells + # Second chart: Plot asset price, moving averages and buys/sells ax2 = plt.subplot(412, sharex=ax1) perf.loc[:, ['price','short_mavg','long_mavg']].plot(ax=ax2, label='Price') ax2.legend_.remove() @@ -119,7 +119,7 @@ def analyze(context, perf): label='' ) - # Third subgraph: Compare percentage change between our portfolio + # Third chart: Compare percentage change between our portfolio # and the price of the asset ax3 = plt.subplot(413, sharex=ax1) perf.loc[:, ['algorithm_period_return', 'price_change']].plot(ax=ax3) @@ -128,7 +128,7 @@ def analyze(context, perf): start, end = ax3.get_ylim() ax3.yaxis.set_ticks(np.arange(start, end, (end-start)/5)) - # Plot our cash + # Fourth chart: Plot our cash ax4 = plt.subplot(414, sharex=ax1) perf.cash.plot(ax=ax4) ax4.set_ylabel('Cash\n({})'.format(base_currency)) From dfcfe5a370eed23c095b938306285496eeafb232 Mon Sep 17 00:00:00 2001 From: fredfortier Date: Tue, 28 Nov 2017 01:44:46 -0500 Subject: [PATCH 05/11] merging from develop --- catalyst/examples/mean_reversion_simple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalyst/examples/mean_reversion_simple.py b/catalyst/examples/mean_reversion_simple.py index 6d4922e1..75e9f2ea 100644 --- a/catalyst/examples/mean_reversion_simple.py +++ b/catalyst/examples/mean_reversion_simple.py @@ -248,7 +248,7 @@ if __name__ == '__main__': # catalyst run -f catalyst/examples/mean_reversion_simple.py -x poloniex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000 run_algorithm( capital_base=10000, - data_frequency='minute', + data_frequency='daily', initialize=initialize, handle_data=handle_data, analyze=analyze, From ffd1bc07cc973a9c3655bed0ed6b05ec21f44f2c Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Tue, 28 Nov 2017 11:22:41 -0700 Subject: [PATCH 06/11] DOC: making example algos consistent with doc website --- catalyst/examples/buy_and_hodl.py | 25 ++++++++++--------------- catalyst/examples/buy_btc_simple.py | 5 +++-- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/catalyst/examples/buy_and_hodl.py b/catalyst/examples/buy_and_hodl.py index 536de1cd..74b04238 100644 --- a/catalyst/examples/buy_and_hodl.py +++ b/catalyst/examples/buy_and_hodl.py @@ -15,15 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. import pandas as pd +import matplotlib.pyplot as plt from catalyst import run_algorithm -from catalyst.api import ( - order_target_value, - symbol, - record, - cancel_order, - get_open_orders, -) +from catalyst.api import (order_target_value, symbol, record, + cancel_order, get_open_orders, ) def initialize(context): @@ -78,15 +74,14 @@ def handle_data(context, data): def analyze(context=None, results=None): - import matplotlib.pyplot as plt # Plot the portfolio and asset data. ax1 = plt.subplot(611) results[['portfolio_value']].plot(ax=ax1) - ax1.set_ylabel('Portfolio Value (USD)') + ax1.set_ylabel('Portfolio\nValue\n(USD)') ax2 = plt.subplot(612, sharex=ax1) - ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME)) + ax2.set_ylabel('{asset}\n(USD)'.format(asset=context.ASSET_NAME)) results[['price']].plot(ax=ax2) trans = results.ix[[t != [] for t in results.transactions]] @@ -126,11 +121,11 @@ def analyze(context=None, results=None): 'algorithm', 'benchmark', ]].plot(ax=ax5) - ax5.set_ylabel('Percent Change') + ax5.set_ylabel('Percent\nChange') ax6 = plt.subplot(616, sharex=ax1) results[['volume']].plot(ax=ax6) - ax6.set_ylabel('Volume (mCoins/5min)') + ax6.set_ylabel('Volume') plt.legend(loc=3) @@ -142,13 +137,13 @@ def analyze(context=None, results=None): if __name__ == '__main__': run_algorithm( capital_base=10000, - data_frequency='minute', + data_frequency='daily', initialize=initialize, handle_data=handle_data, analyze=analyze, exchange_name='bitfinex', algo_namespace='buy_and_hodl', base_currency='usd', - start=pd.to_datetime('2017-11-01', utc=True), - end=pd.to_datetime('2017-11-10', utc=True), + start=pd.to_datetime('2015-03-01', utc=True), + end=pd.to_datetime('2017-10-31', utc=True), ) diff --git a/catalyst/examples/buy_btc_simple.py b/catalyst/examples/buy_btc_simple.py index ef64513b..5e4ebd57 100644 --- a/catalyst/examples/buy_btc_simple.py +++ b/catalyst/examples/buy_btc_simple.py @@ -3,7 +3,8 @@ https://enigmampc.github.io/catalyst/beginner-tutorial.html Run this example, by executing the following from your terminal: - catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle + catalyst ingest-exchange -x bitfinex -f daily -i btc_usdt + catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle If you want to run this code using another exchange, make sure that the asset is available on that exchange. For example, if you were to run @@ -12,7 +13,7 @@ context.asset = symbol('btc_usdt') # note 'usdt' instead of 'usd' and specify exchange poloniex as follows: - + catalyst ingest-exchange -x poloniex -f daily -i btc_usdt catalyst run -f buy_btc_simple.py -x poloniex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle To see which assets are available on each exchange, visit: From b63199e4e154c70e2308256d0164cfabf2f15a54 Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Tue, 28 Nov 2017 12:24:53 -0700 Subject: [PATCH 07/11] DOC: adjusting example params to match video tutorial --- catalyst/examples/mean_reversion_simple.py | 30 ++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/catalyst/examples/mean_reversion_simple.py b/catalyst/examples/mean_reversion_simple.py index 75e9f2ea..0fcacb21 100644 --- a/catalyst/examples/mean_reversion_simple.py +++ b/catalyst/examples/mean_reversion_simple.py @@ -5,6 +5,7 @@ import os import tempfile import time +import numpy as np import pandas as pd import talib from logbook import Logger @@ -31,14 +32,14 @@ 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 USD Tether. - context.neo_eth = symbol('neo_eth') + # In our example, we're looking at Neo in USD. + context.neo_eth = symbol('neo_usd') context.base_price = None context.current_day = None - context.RSI_OVERSOLD = 50 + context.RSI_OVERSOLD = 30 context.RSI_OVERBOUGHT = 80 - context.CANDLE_SIZE = '5T' + context.CANDLE_SIZE = '15T' context.start_time = time.time() @@ -160,13 +161,13 @@ def analyze(context=None, perf=None): # Plot the portfolio value over time. ax1 = plt.subplot(611) perf.loc[:, 'portfolio_value'].plot(ax=ax1) - ax1.set_ylabel('Portfolio Value ({})'.format(base_currency)) + ax1.set_ylabel('Portfolio\nValue\n({})'.format(base_currency)) # Plot the price increase or decrease over time. ax2 = plt.subplot(612, sharex=ax1) perf.loc[:, 'price'].plot(ax=ax2, label='Price') - ax2.set_ylabel('{asset} ({base})'.format( + ax2.set_ylabel('{asset}\n({base})'.format( asset=context.neo_eth.symbol, base=base_currency )) @@ -195,18 +196,19 @@ def analyze(context=None, perf=None): perf.loc[:, 'cash'].plot( ax=ax4, label='Base Currency ({})'.format(base_currency) ) - ax4.set_ylabel('Cash ({})'.format(base_currency)) + ax4.set_ylabel('Cash\n({})'.format(base_currency)) perf['algorithm'] = perf.loc[:, 'algorithm_period_return'] ax5 = plt.subplot(614, sharex=ax1) perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5) - ax5.set_ylabel('Percent Change') + ax5.set_ylabel('Percent\nChange') ax6 = plt.subplot(615, sharex=ax1) perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI') - ax6.axhline(70, color='darkgoldenrod') - ax6.axhline(30, color='darkgoldenrod') + ax6.set_ylabel('RSI') + ax6.axhline(context.RSI_OVERBOUGHT, color='darkgoldenrod') + ax6.axhline(context.RSI_OVERSOLD, color='darkgoldenrod') if not transaction_df.empty: ax6.scatter( @@ -226,6 +228,8 @@ def analyze(context=None, perf=None): label='' ) plt.legend(loc=3) + start, end = ax6.get_ylim() + ax6.yaxis.set_ticks(np.arange(0, end, end/5)) # Show the plot. plt.gcf().set_size_inches(18, 8) @@ -245,10 +249,10 @@ if __name__ == '__main__': timestr = time.strftime('%Y%m%d-%H%M%S') out = os.path.join(folder, '{}.p'.format(timestr)) - # catalyst run -f catalyst/examples/mean_reversion_simple.py -x poloniex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000 + # catalyst run -f catalyst/examples/mean_reversion_simple.py -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000 run_algorithm( capital_base=10000, - data_frequency='daily', + data_frequency='minute', initialize=initialize, handle_data=handle_data, analyze=analyze, @@ -270,6 +274,6 @@ if __name__ == '__main__': exchange_name='bittrex', live=True, algo_namespace=NAMESPACE, - base_currency='eth', + base_currency='usd', live_graph=False ) From 7cf4f84e8992815bb0055c96d95e479ae89a6142 Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Tue, 28 Nov 2017 12:35:50 -0700 Subject: [PATCH 08/11] DOC: documented 4 example algorithms --- catalyst/examples/README.rst | 3 + docs/source/beginner-tutorial.rst | 1 + docs/source/example-algos.rst | 973 ++++++++++++++++++++---------- 3 files changed, 643 insertions(+), 334 deletions(-) create mode 100644 catalyst/examples/README.rst diff --git a/catalyst/examples/README.rst b/catalyst/examples/README.rst new file mode 100644 index 00000000..d75c9249 --- /dev/null +++ b/catalyst/examples/README.rst @@ -0,0 +1,3 @@ +An overview of most of the trading strategies in this folder can be found in the +`Examples Algorithms `_ +section of our documentation website. \ No newline at end of file diff --git a/docs/source/beginner-tutorial.rst b/docs/source/beginner-tutorial.rst index 6743f82d..38603888 100644 --- a/docs/source/beginner-tutorial.rst +++ b/docs/source/beginner-tutorial.rst @@ -546,6 +546,7 @@ only bought bitcoin every chance it got. sudo apt install python-tk +.. _history: Access to previous prices using ``history`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/example-algos.rst b/docs/source/example-algos.rst index e341f331..002dc46b 100644 --- a/docs/source/example-algos.rst +++ b/docs/source/example-algos.rst @@ -2,28 +2,102 @@ Example Algorithms ================== -This section documents a small number of example algorithms to complement the +This section documents a number of example algorithms to complement the beginner tutorial, and show how other trading algorithms can be implemented -using Catalyst: +using Catalyst. + +Overview +~~~~~~~~ + +- :ref:`Buy BTC Simple`: The simplest algorithm that introduces + the ``initialize()`` and ``handle_data()`` functions, and is used in the + :doc:`beginner tutorial` to show how to run catalyst + for the first time. + +- :ref:`Buy and Hodl `: A very straightforward *buy and hold* that + makes one single buy at the very beginning. Introduces the notions of + ``cash``, management of outstanding ``orders``, and ``order_target_value`` + to place orders. It also introduces the ``analyze()`` function to visualize + the performance of our strategy using the external library ``matplotlib``. + +- :ref:`Dual Moving Average Crossover`: A classic momentum + strategy used in the second part of the + `beginner tutorial `_ to introduce the + ``data.history()`` function. It makes a heavy use of ``matplotlib`` library + in the ``analyze()`` function to chart the performance of the algorithm. + +- :ref:`Mean Reversion Algorithm `: Another simple momentum + strategy that is used in our + `two-part video tutorial `_ to show how + to get started in backtesting and live trading with Catalyst. + + +.. _buy_btc_simple: + +Buy BTC Simple Algorithm +~~~~~~~~~~~~~~~~~~~~~~~~ + +Source code: `examples/buy_btc_simple.py `_ + +.. code-block:: python + + ''' + Run this example, by executing the following from your terminal: + catalyst ingest-exchange -x bitfinex -f daily -i btc_usdt + catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle + + If you want to run this code using another exchange, make sure that + the asset is available on that exchange. For example, if you were to run + it for exchange Poloniex, you would need to edit the following line: + + context.asset = symbol('btc_usdt') # note 'usdt' instead of 'usd' + + and specify exchange poloniex as follows: + catalyst ingest-exchange -x poloniex -f daily -i btc_usdt + catalyst run -f buy_btc_simple.py -x poloniex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle + + To see which assets are available on each exchange, visit: + https://www.enigma.co/catalyst/status + ''' + + from catalyst.api import order, record, symbol + + def initialize(context): + context.asset = symbol('btc_usd') + + def handle_data(context, data): + order(context.asset, 1) + record(btc = data.current(context.asset, 'price')) + +This simple algorithm does not produce any output nor displays any chart. + .. _buy_and_hodl: Buy and Hodl Algorithm ~~~~~~~~~~~~~~~~~~~~~~ -source: `examples/buy_and_hodl.py `_ +Source code: `examples/buy_and_hodl.py `_ First ingest the historical pricing data needed to run this algorithm: .. code-block:: bash - catalyst ingest-exchange -x poloniex -f daily -i btc_usdt + catalyst ingest-exchange -x bitfinex -f daily -i btc_usd Then, you can run the code below with the following command: .. code-block:: bash - catalyst run -f buy_and_hodl.py --start 2015-3-1 --end 2017-10-31 --capital-base 100000 -x poloniex -c btc -o bah.pickle + catalyst run -f buy_and_hodl.py --start 2015-3-1 --end 2017-10-31 --capital-base 100000 -x bitfinex -c btc -o bah.pickle + +or using the same parameters specified in the run_algorithm() function at the +end of the file: + +.. code-block:: bash + + python buy_and_hodl.py + This command will run the trading algorithm in the specified time range and plot the resulting performance using the matplotlib library. You can choose any @@ -35,156 +109,340 @@ one day prior to the current date. .. code-block:: python - #!/usr/bin/env python - # - # Copyright 2017 Enigma MPC, Inc. - # Copyright 2015 Quantopian, Inc. - # - # Licensed under the Apache License, Version 2.0 (the "License"); - # you may not use this file except in compliance with the License. - # You may obtain a copy of the License at - # - # http://www.apache.org/licenses/LICENSE-2.0 - # - # Unless required by applicable law or agreed to in writing, software - # distributed under the License is distributed on an "AS IS" BASIS, - # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - # See the License for the specific language governing permissions and - # limitations under the License. - import pandas as pd + #!/usr/bin/env python + # + # Copyright 2017 Enigma MPC, Inc. + # Copyright 2015 Quantopian, Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + import pandas as pd + import matplotlib.pyplot as plt - from catalyst.api import ( - order_target_value, - symbol, - record, - cancel_order, - get_open_orders, - ) + from catalyst import run_algorithm + from catalyst.api import (order_target_value, symbol, record, + cancel_order, get_open_orders, ) - def initialize(context): - context.ASSET_NAME = 'btc_usdt' - context.TARGET_HODL_RATIO = 0.8 - context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO + def initialize(context): + context.ASSET_NAME = 'btc_usd' + context.TARGET_HODL_RATIO = 0.8 + context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO - context.is_buying = True - context.asset = symbol(context.ASSET_NAME) + context.is_buying = True + context.asset = symbol(context.ASSET_NAME) - context.i = 0 + context.i = 0 - def handle_data(context, data): - context.i += 1 + def handle_data(context, data): + context.i += 1 - starting_cash = context.portfolio.starting_cash - target_hodl_value = context.TARGET_HODL_RATIO * starting_cash - reserve_value = context.RESERVE_RATIO * starting_cash + starting_cash = context.portfolio.starting_cash + target_hodl_value = context.TARGET_HODL_RATIO * starting_cash + reserve_value = context.RESERVE_RATIO * starting_cash - # Cancel any outstanding orders - orders = get_open_orders(context.asset) or [] - for order in orders: - cancel_order(order) + # Cancel any outstanding orders + orders = get_open_orders(context.asset) or [] + for order in orders: + cancel_order(order) - # Stop buying after passing the reserve threshold - cash = context.portfolio.cash - if cash <= reserve_value: - context.is_buying = False + # Stop buying after passing the reserve threshold + cash = context.portfolio.cash + if cash <= reserve_value: + context.is_buying = False - # Retrieve current asset price from pricing data - price = data.current(context.asset, 'price') + # Retrieve current asset price from pricing data + price = data.current(context.asset, 'price') - # Check if still buying and could (approximately) afford another purchase - if context.is_buying and cash > price: - # Place order to make position in asset equal to target_hodl_value - order_target_value( - context.asset, - target_hodl_value, - limit_price=price * 1.1, - stop_price=price * 0.9, - ) + # Check if still buying and could (approximately) afford another purchase + if context.is_buying and cash > price: + print('buying') + # Place order to make position in asset equal to target_hodl_value + order_target_value( + context.asset, + target_hodl_value, + limit_price=price * 1.1, + stop_price=price * 0.9, + ) - record( - price=price, - volume=data.current(context.asset, 'volume'), - cash=cash, - starting_cash=context.portfolio.starting_cash, - leverage=context.account.leverage, - ) + record( + price=price, + volume=data.current(context.asset, 'volume'), + cash=cash, + starting_cash=context.portfolio.starting_cash, + leverage=context.account.leverage, + ) - def analyze(context=None, results=None): - import matplotlib.pyplot as plt + def analyze(context=None, results=None): - # Plot the portfolio and asset data. - ax1 = plt.subplot(611) - results[['portfolio_value']].plot(ax=ax1) - ax1.set_ylabel('Portfolio Value (USD)') + # Plot the portfolio and asset data. + ax1 = plt.subplot(611) + results[['portfolio_value']].plot(ax=ax1) + ax1.set_ylabel('Portfolio Value (USD)') - ax2 = plt.subplot(612, sharex=ax1) - ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME)) - results[['price']].plot(ax=ax2) + ax2 = plt.subplot(612, sharex=ax1) + ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME)) + results[['price']].plot(ax=ax2) - trans = results.ix[[t != [] for t in results.transactions]] - buys = trans.ix[ - [t[0]['amount'] > 0 for t in trans.transactions] - ] - ax2.plot( - buys.index, - results.price[buys.index], - '^', - markersize=10, - color='g', - ) + trans = results.ix[[t != [] for t in results.transactions]] + buys = trans.ix[ + [t[0]['amount'] > 0 for t in trans.transactions] + ] + ax2.scatter( + buys.index.to_pydatetime(), + results.price[buys.index], + marker='^', + s=100, + c='g', + label='' + ) - ax3 = plt.subplot(613, sharex=ax1) - results[['leverage', 'alpha', 'beta']].plot(ax=ax3) - ax3.set_ylabel('Leverage ') + ax3 = plt.subplot(613, sharex=ax1) + results[['leverage', 'alpha', 'beta']].plot(ax=ax3) + ax3.set_ylabel('Leverage ') - ax4 = plt.subplot(614, sharex=ax1) - results[['starting_cash', 'cash']].plot(ax=ax4) - ax4.set_ylabel('Cash (USD)') + ax4 = plt.subplot(614, sharex=ax1) + results[['starting_cash', 'cash']].plot(ax=ax4) + ax4.set_ylabel('Cash (USD)') - results[[ - 'treasury', - 'algorithm', - 'benchmark', - ]] = results[[ - 'treasury_period_return', - 'algorithm_period_return', - 'benchmark_period_return', - ]] + results[[ + 'treasury', + 'algorithm', + 'benchmark', + ]] = results[[ + 'treasury_period_return', + 'algorithm_period_return', + 'benchmark_period_return', + ]] - ax5 = plt.subplot(615, sharex=ax1) - results[[ - 'treasury', - 'algorithm', - 'benchmark', - ]].plot(ax=ax5) - ax5.set_ylabel('Percent Change') + ax5 = plt.subplot(615, sharex=ax1) + results[[ + 'treasury', + 'algorithm', + 'benchmark', + ]].plot(ax=ax5) + ax5.set_ylabel('Percent Change') - ax6 = plt.subplot(616, sharex=ax1) - results[['volume']].plot(ax=ax6) - ax6.set_ylabel('Volume (mCoins/5min)') + ax6 = plt.subplot(616, sharex=ax1) + results[['volume']].plot(ax=ax6) + ax6.set_ylabel('Volume (mCoins/5min)') - plt.legend(loc=3) + plt.legend(loc=3) + + # Show the plot. + plt.gcf().set_size_inches(18, 8) + plt.show() + + + if __name__ == '__main__': + run_algorithm( + capital_base=10000, + data_frequency='daily', + initialize=initialize, + handle_data=handle_data, + analyze=analyze, + exchange_name='bitfinex', + algo_namespace='buy_and_hodl', + base_currency='usd', + start=pd.to_datetime('2015-03-01', utc=True), + end=pd.to_datetime('2017-10-31', utc=True), + ) + +.. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/example_buy_and_hodl.png + +.. _dual_moving_average: + +Dual Moving Average Crossover +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Source Code: `examples/dual_moving_average.py `_ + +This strategy is covered in detail in the last part of +`this tutorial `_. + +.. code-block:: python + + import numpy as np + import pandas as pd + from logbook import Logger + import matplotlib.pyplot as plt + + from catalyst import run_algorithm + from catalyst.api import (order, record, symbol, order_target_percent, + get_open_orders) + from catalyst.exchange.stats_utils import extract_transactions + + NAMESPACE = 'dual_moving_average' + log = Logger(NAMESPACE) + + def initialize(context): + context.i = 0 + context.asset = symbol('ltc_usd') + context.base_price = None + + + def handle_data(context, data): + # define the windows for the moving averages + short_window = 50 + long_window = 200 + + # Skip as many bars as long_window to properly compute the average + context.i += 1 + if context.i < long_window: + return + + # Compute moving averages calling data.history() for each + # moving average with the appropriate parameters. We choose to use + # minute bars for this simulation -> freq="1m" + # Returns a pandas dataframe. + short_mavg = data.history(context.asset, 'price', + bar_count=short_window, frequency="1m").mean() + long_mavg = data.history(context.asset, 'price', + bar_count=long_window, frequency="1m").mean() + + # Let's keep the price of our asset in a more handy variable + price = data.current(context.asset, 'price') + + # 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 + + # Save values for later inspection + record(price=price, + cash=context.portfolio.cash, + price_change=price_change, + short_mavg=short_mavg, + long_mavg=long_mavg) + + # 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.asset) + if len(orders) > 0: + return + + # Exit if we cannot trade + if not data.can_trade(context.asset): + return + + # We check what's our position on our portfolio and trade accordingly + pos_amount = context.portfolio.positions[context.asset].amount + + # Trading logic + if short_mavg > long_mavg and pos_amount == 0: + # we buy 100% of our portfolio for this asset + order_target_percent(context.asset, 1) + elif short_mavg < long_mavg and pos_amount > 0: + # we sell all our positions for this asset + order_target_percent(context.asset, 0) + + + def analyze(context, perf): + + # Get the base_currency that was passed as a parameter to the simulation + base_currency = context.exchanges.values()[0].base_currency.upper() + + # First chart: Plot portfolio value using base_currency + ax1 = plt.subplot(411) + perf.loc[:, ['portfolio_value']].plot(ax=ax1) + ax1.legend_.remove() + ax1.set_ylabel('Portfolio Value\n({})'.format(base_currency)) + start, end = ax1.get_ylim() + ax1.yaxis.set_ticks(np.arange(start, end, (end-start)/5)) + + # Second chart: Plot asset price, moving averages and buys/sells + ax2 = plt.subplot(412, sharex=ax1) + perf.loc[:, ['price','short_mavg','long_mavg']].plot(ax=ax2, label='Price') + ax2.legend_.remove() + ax2.set_ylabel('{asset}\n({base})'.format( + asset = context.asset.symbol, + base = base_currency + )) + start, end = ax2.get_ylim() + ax2.yaxis.set_ticks(np.arange(start, end, (end-start)/5)) + + transaction_df = extract_transactions(perf) + if not transaction_df.empty: + buy_df = transaction_df[transaction_df['amount'] > 0] + sell_df = transaction_df[transaction_df['amount'] < 0] + ax2.scatter( + buy_df.index.to_pydatetime(), + perf.loc[buy_df.index, 'price'], + marker='^', + s=100, + c='green', + label='' + ) + ax2.scatter( + sell_df.index.to_pydatetime(), + perf.loc[sell_df.index, 'price'], + marker='v', + s=100, + c='red', + label='' + ) + + # Third chart: Compare percentage change between our portfolio + # and the price of the asset + ax3 = plt.subplot(413, sharex=ax1) + perf.loc[:, ['algorithm_period_return', 'price_change']].plot(ax=ax3) + ax3.legend_.remove() + ax3.set_ylabel('Percent Change') + start, end = ax3.get_ylim() + ax3.yaxis.set_ticks(np.arange(start, end, (end-start)/5)) + + # Fourth chart: Plot our cash + ax4 = plt.subplot(414, sharex=ax1) + perf.cash.plot(ax=ax4) + ax4.set_ylabel('Cash\n({})'.format(base_currency)) + start, end = ax4.get_ylim() + ax4.yaxis.set_ticks(np.arange(0, end, end/5)) + + plt.show() + + + if __name__ == '__main__': + run_algorithm( + capital_base=1000, + data_frequency='minute', + initialize=initialize, + handle_data=handle_data, + analyze=analyze, + exchange_name='bitfinex', + algo_namespace=NAMESPACE, + base_currency='usd', + start=pd.to_datetime('2017-9-22', utc=True), + end=pd.to_datetime('2017-9-23', utc=True), + ) + +.. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/tutorial_dual_moving_average.png - # Show the plot. - plt.gcf().set_size_inches(18, 8) - plt.show() .. _mean_reversion: Mean Reversion Algorithm ~~~~~~~~~~~~~~~~~~~~~~~~ -source: `examples/mean_reversion_simple.py `_ +Source code: `examples/mean_reversion_simple.py `_ This algorithm is based on a simple momentum strategy. When the cryptoasset goes up quickly, we're going to buy; when it goes down quickly, we're going to sell. Hopefully, we'll ride the waves. -We are choosing to run this trading algorithm with the ``neo_usd`` currency pair -on the ``Bitfinex`` exchange. Thus, first ingest the historical pricing data +We are choosing to backtest this trading algorithm with the ``neo_usd`` currency +pairon the ``Bitfinex`` exchange. Thus, first ingest the historical pricing data that we need, with minute resolution: .. code-block:: bash @@ -201,244 +459,291 @@ lines 218-245, so in order to run the algorithm we just type: .. code-block:: python - import pandas as pd - import talib - from logbook import Logger + import os + import tempfile + import time - from catalyst import run_algorithm - from catalyst.api import symbol, record, order_target_percent, get_open_orders - from catalyst.exchange.stats_utils import extract_transactions + import numpy as np + import pandas as pd + import talib + from logbook import Logger - # We give a name to the algorithm which Catalyst will use to persist its state. - # 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. - NAMESPACE = 'mean_reversion_simple' - log = Logger(NAMESPACE) + from catalyst import run_algorithm + from catalyst.api import symbol, record, order_target_percent, get_open_orders + from catalyst.exchange.stats_utils import extract_transactions + # We give a name to the algorithm which Catalyst will use to persist its state. + # 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.utils.paths import ensure_directory - # To run an algorithm in Catalyst, you need two functions: initialize and - # handle_data. - - def initialize(context): - # This initialize function sets any data or variables that you'll use in - # your algorithm. For instance, you'll want to define the trading pair (or - # 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 USD Tether. - context.neo_usd = symbol('neo_usd') - context.base_price = None - context.current_day = None + NAMESPACE = 'mean_reversion_simple' + log = Logger(NAMESPACE) - def handle_data(context, data): - # This handle_data function is where the real work is done. Our data is - # minute-level tick data, and each minute is called a frame. This function - # runs on each frame of the data. + # To run an algorithm in Catalyst, you need two functions: initialize and + # handle_data. - # We flag the first period of each day. - # Since cryptocurrencies trade 24/7 the `before_trading_starts` handle - # 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 - context.current_day = today + def initialize(context): + # This initialize function sets any data or variables that you'll use in + # your algorithm. For instance, you'll want to define the trading pair (or + # trading pairs) you want to backtest. You'll also want to define any + # parameters or values you're going to use. - # We're computing the volume-weighted-average-price of the security - # defined above, in the context.neo_usd variable. For this example, we're - # using three bars on the 15 min bars. + # In our example, we're looking at Neo in USD. + context.neo_eth = symbol('neo_usd') + context.base_price = None + context.current_day = None - # 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.neo_usd, - fields='close', - bar_count=50, - frequency='15T' - ) + context.RSI_OVERSOLD = 30 + context.RSI_OVERBOUGHT = 80 + context.CANDLE_SIZE = '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=14) - - # We need a variable for the current price of the security to compare to - # the average. Since we are requesting two fields, data.current() - # returns a DataFrame with - current = data.current(context.neo_usd, fields=['close', 'volume']) - price = current['close'] - - # 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 - 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'], - 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: - 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.neo_usd) - if len(orders) > 0: - return - - # Exit if we cannot trade - if not data.can_trade(context.neo_usd): - return - - # Another powerful built-in feature of the Catalyst backtester is the - # portfolio object. The portfolio object tracks your positions, cash, - # cost basis of specific holdings, and more. In this line, we calculate - # how long or short our position is at this minute. - pos_amount = context.portfolio.positions[context.neo_usd].amount - - if rsi[-1] <= 30 and pos_amount == 0: - log.info( - '{}: buying - price: {}, rsi: {}'.format( - data.current_dt, price, rsi[-1] - ) - ) - order_target_percent(context.neo_usd, 1) - context.traded_today = True - - elif rsi[-1] >= 80 and pos_amount > 0: - log.info( - '{}: selling - price: {}, rsi: {}'.format( - data.current_dt, price, rsi[-1] - ) - ) - order_target_percent(context.neo_usd, 0) - context.traded_today = True + context.start_time = time.time() - def analyze(context=None, perf=None): - import matplotlib.pyplot as plt + def handle_data(context, data): + # This handle_data function is where the real work is done. Our data is + # minute-level tick data, and each minute is called a frame. This function + # runs on each frame of the data. - # The base currency of the algo exchange - base_currency = context.exchanges.values()[0].base_currency.upper() + # We flag the first period of each day. + # Since cryptocurrencies trade 24/7 the `before_trading_starts` handle + # 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 + context.current_day = today - # Plot the portfolio value over time. - ax1 = plt.subplot(611) - perf.loc[:, 'portfolio_value'].plot(ax=ax1) - ax1.set_ylabel('Portfolio Value ({})'.format(base_currency)) + # We're computing the volume-weighted-average-price of the security + # defined above, in the context.neo_eth variable. For this example, we're + # using three bars on the 15 min bars. - # Plot the price increase or decrease over time. - ax2 = plt.subplot(612, sharex=ax1) - perf.loc[:, 'price'].plot(ax=ax2, label='Price') + # 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.neo_eth, + fields='close', + bar_count=50, + frequency=context.CANDLE_SIZE + ) - ax2.set_ylabel('{asset} ({base})'.format( - asset=context.neo_usd.symbol, base=base_currency - )) + # Ta-lib calculates various technical indicator based on price and + # volume arrays. - transaction_df = extract_transactions(perf) - if not transaction_df.empty: - buy_df = transaction_df[transaction_df['amount'] > 0] - sell_df = transaction_df[transaction_df['amount'] < 0] - ax2.scatter( - buy_df.index.to_pydatetime(), - perf.loc[buy_df.index, 'price'], - marker='^', - s=100, - c='green', - label='' - ) - ax2.scatter( - sell_df.index.to_pydatetime(), - perf.loc[sell_df.index, 'price'], - marker='v', - s=100, - c='red', - label='' - ) + # In this example, we are comp + rsi = talib.RSI(prices.values, timeperiod=14) - ax4 = plt.subplot(613, sharex=ax1) - perf.loc[:, 'cash'].plot( - ax=ax4, label='Base Currency ({})'.format(base_currency) - ) - ax4.set_ylabel('Cash ({})'.format(base_currency)) + # We need a variable for the current price of the security to compare to + # the average. Since we are requesting two fields, data.current() + # returns a DataFrame with + current = data.current(context.neo_eth, fields=['close', 'volume']) + price = current['close'] - perf['algorithm'] = perf.loc[:, 'algorithm_period_return'] + # 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 - ax5 = plt.subplot(614, sharex=ax1) - perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5) - ax5.set_ylabel('Percent Change') + price_change = (price - context.base_price) / context.base_price + cash = context.portfolio.cash - ax6 = plt.subplot(615, sharex=ax1) - perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI') - ax6.axhline(70, color='darkgoldenrod') - ax6.axhline(30, color='darkgoldenrod') + # 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'], + price_change=price_change, + rsi=rsi[-1], + cash=cash + ) - if not transaction_df.empty: - ax6.scatter( - buy_df.index.to_pydatetime(), - perf.loc[buy_df.index, 'rsi'], - marker='^', - s=100, - c='green', - label='' - ) - ax6.scatter( - sell_df.index.to_pydatetime(), - perf.loc[sell_df.index, 'rsi'], - marker='v', - s=100, - c='red', - label='' - ) - plt.legend(loc=3) + # We are trying to avoid over-trading by limiting our trades to + # one per day. + if context.traded_today: + return - # Show the plot. - plt.gcf().set_size_inches(18, 8) - plt.show() - pass + # 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.neo_eth) + if len(orders) > 0: + return + + # Exit if we cannot trade + if not data.can_trade(context.neo_eth): + return + + # Another powerful built-in feature of the Catalyst backtester is the + # portfolio object. The portfolio object tracks your positions, cash, + # cost basis of specific holdings, and more. In this line, we calculate + # how long or short our position is at this minute. + pos_amount = context.portfolio.positions[context.neo_eth].amount + + if rsi[-1] <= context.RSI_OVERSOLD and pos_amount == 0: + log.info( + '{}: buying - price: {}, rsi: {}'.format( + data.current_dt, price, rsi[-1] + ) + ) + # Set a style for limit orders, + limit_price = price * 1.005 + order_target_percent( + context.neo_eth, 1, limit_price=limit_price + ) + context.traded_today = True + + elif rsi[-1] >= context.RSI_OVERBOUGHT and pos_amount > 0: + log.info( + '{}: selling - price: {}, rsi: {}'.format( + data.current_dt, price, rsi[-1] + ) + ) + limit_price = price * 0.995 + order_target_percent( + context.neo_eth, 0, limit_price=limit_price + ) + context.traded_today = True - if __name__ == '__main__': - # The execution mode: backtest or live - MODE = 'backtest' + def analyze(context=None, perf=None): + end = time.time() + log.info('elapsed time: {}'.format(end - context.start_time)) + + import matplotlib.pyplot as plt + # The base currency of the algo exchange + base_currency = context.exchanges.values()[0].base_currency.upper() + + # Plot the portfolio value over time. + ax1 = plt.subplot(611) + perf.loc[:, 'portfolio_value'].plot(ax=ax1) + ax1.set_ylabel('Portfolio\nValue\n({})'.format(base_currency)) + + # Plot the price increase or decrease over time. + ax2 = plt.subplot(612, sharex=ax1) + perf.loc[:, 'price'].plot(ax=ax2, label='Price') + + ax2.set_ylabel('{asset}\n({base})'.format( + asset=context.neo_eth.symbol, base=base_currency + )) + + transaction_df = extract_transactions(perf) + if not transaction_df.empty: + buy_df = transaction_df[transaction_df['amount'] > 0] + sell_df = transaction_df[transaction_df['amount'] < 0] + ax2.scatter( + buy_df.index.to_pydatetime(), + perf.loc[buy_df.index.floor('1 min'), 'price'], + marker='^', + s=100, + c='green', + label='' + ) + ax2.scatter( + sell_df.index.to_pydatetime(), + perf.loc[sell_df.index.floor('1 min'), 'price'], + marker='v', + s=100, + c='red', + label='' + ) + + ax4 = plt.subplot(613, sharex=ax1) + perf.loc[:, 'cash'].plot( + ax=ax4, label='Base Currency ({})'.format(base_currency) + ) + ax4.set_ylabel('Cash\n({})'.format(base_currency)) + + perf['algorithm'] = perf.loc[:, 'algorithm_period_return'] + + ax5 = plt.subplot(614, sharex=ax1) + perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5) + ax5.set_ylabel('Percent\nChange') + + ax6 = plt.subplot(615, sharex=ax1) + perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI') + ax6.set_ylabel('RSI') + ax6.axhline(context.RSI_OVERBOUGHT, color='darkgoldenrod') + ax6.axhline(context.RSI_OVERSOLD, color='darkgoldenrod') + + if not transaction_df.empty: + ax6.scatter( + buy_df.index.to_pydatetime(), + perf.loc[buy_df.index.floor('1 min'), 'rsi'], + marker='^', + s=100, + c='green', + label='' + ) + ax6.scatter( + sell_df.index.to_pydatetime(), + perf.loc[sell_df.index.floor('1 min'), 'rsi'], + marker='v', + s=100, + c='red', + label='' + ) + plt.legend(loc=3) + start, end = ax6.get_ylim() + ax6.yaxis.set_ticks(np.arange(0, end, end/5)) + + # Show the plot. + plt.gcf().set_size_inches(18, 8) + plt.show() + pass + + + if __name__ == '__main__': + # The execution mode: backtest or live + MODE = 'backtest' + + if MODE == 'backtest': + folder = os.path.join( + tempfile.gettempdir(), 'catalyst', NAMESPACE + ) + ensure_directory(folder) + + timestr = time.strftime('%Y%m%d-%H%M%S') + out = os.path.join(folder, '{}.p'.format(timestr)) + # catalyst run -f catalyst/examples/mean_reversion_simple.py -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000 + run_algorithm( + capital_base=10000, + data_frequency='minute', + initialize=initialize, + handle_data=handle_data, + analyze=analyze, + exchange_name='bitfinex', + algo_namespace=NAMESPACE, + base_currency='usd', + start=pd.to_datetime('2017-10-01', utc=True), + end=pd.to_datetime('2017-11-10', utc=True), + output=out + ) + log.info('saved perf stats: {}'.format(out)) + + elif MODE == 'live': + run_algorithm( + capital_base=0.5, + initialize=initialize, + handle_data=handle_data, + analyze=analyze, + exchange_name='bittrex', + live=True, + algo_namespace=NAMESPACE, + base_currency='usd', + live_graph=False + ) + +.. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/example_mean_reversion_simple.png + +Notice the difference in performance between the charts above and those seen on +`this video tutorial `_ at +minute 8:10. The buy and sell orders are triggered at the same exact times, but +the differences result from a more realistic slippage model +implemented after the video was recorded, which executes the orders at slighlty +different prices, but resulting in significant changes in performance of our +strategy. - if MODE == 'backtest': - # catalyst run -f catalyst/examples/mean_reversion_simple.py -x poloniex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000 - run_algorithm( - capital_base=10000, - data_frequency='minute', - initialize=initialize, - handle_data=handle_data, - analyze=analyze, - exchange_name='bitfinex', - algo_namespace=NAMESPACE, - base_currency='usd', - start=pd.to_datetime('2017-10-1', utc=True), - end=pd.to_datetime('2017-11-10', utc=True), - ) - elif MODE == 'live': - run_algorithm( - initialize=initialize, - handle_data=handle_data, - analyze=analyze, - exchange_name='bitfinex', - live=True, - algo_namespace=NAMESPACE, - base_currency='usd', - live_graph=True - ) From daeccaed364737dece28e48edd9f502ee650da8c Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Tue, 28 Nov 2017 16:44:13 -0700 Subject: [PATCH 09/11] DOC: video - live trading --- docs/source/videos.rst | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/source/videos.rst b/docs/source/videos.rst index d93c09c5..90b53f19 100644 --- a/docs/source/videos.rst +++ b/docs/source/videos.rst @@ -32,7 +32,9 @@ Where things don't: Backtesting a Strategy ---------------------- -This algorithm is based on a simple momentum strategy. When the cryptoasset +This is the first video of a two-part series on using Catalyst for algorithmic +trading. This video implements a simple momentum strategy based on +`mean reversion `_: when the cryptoasset goes up quickly, we’re going to buy; when it goes down quickly, we’re going to sell. Hopefully, we’ll ride the waves. @@ -40,3 +42,17 @@ sell. Hopefully, we’ll ride the waves. +| +| +Live Trading a Strategy +----------------------- + +This is the second part of the two-part series on using Catalyst for algorithmic +trading. Having backtested `our strategy `_ +in the previous video, we now take it to trade live against the Bittrex exchange. + +.. raw:: html + + +| +| \ No newline at end of file From 7abd992d174aa60c4f30bb972f8b1fad3d1db374 Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Wed, 29 Nov 2017 09:19:41 -0700 Subject: [PATCH 10/11] DOC: added portfolio_optimization example --- catalyst/examples/portfolio_optimization.py | 133 ++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 catalyst/examples/portfolio_optimization.py diff --git a/catalyst/examples/portfolio_optimization.py b/catalyst/examples/portfolio_optimization.py new file mode 100644 index 00000000..bb1660a2 --- /dev/null +++ b/catalyst/examples/portfolio_optimization.py @@ -0,0 +1,133 @@ +'''Use this code to execute a portfolio optimization model. This code + will select the portfolio with the maximum Sharpe Ratio. The parameters + are set to use 180 days of historical data and rebalance every 30 days. + + This is the code used in the following article: + https://blog.enigma.co/markowitz-portfolio-optimization-for-cryptocurrencies-in-catalyst-b23c38652556 + + You can run this code using the Python interpreter: + + $ python portfolio_optimization.py +''' + +from __future__ import division +import os +import pytz +import numpy as np +import pandas as pd +from scipy.optimize import minimize +import matplotlib.pyplot as plt +from datetime import datetime + +from catalyst.api import record, symbol, symbols, order_target_percent +from catalyst.utils.run_algo import run_algorithm + +np.set_printoptions(threshold='nan', suppress=True) + + +def initialize(context): + # Portfolio assets list + context.assets = symbols('btc_usdt', 'eth_usdt', 'ltc_usdt', 'dash_usdt', + 'xmr_usdt') + context.nassets = len(context.assets) + # Set the time window that will be used to compute expected return + # and asset correlations + context.window = 180 + # Set the number of days between each portfolio rebalancing + context.rebalance_period = 30 + context.i = 0 + + +def handle_data(context, data): + # Only rebalance at the beggining of the algorithm execution and + # every multiple of the rebalance period + if context.i == 0 or context.i%context.rebalance_period == 0: + n = context.window + prices = data.history(context.assets, fields='price', + bar_count=n+1, frequency='1d') + pr = np.asmatrix(prices) + t_prices = prices.iloc[1:n+1] + t_val = t_prices.values + tminus_prices = prices.iloc[0:n] + tminus_val = tminus_prices.values + # Compute daily returns (r) + r = np.asmatrix(t_val/tminus_val-1) + # Compute the expected returns of each asset with the average + # daily return for the selected time window + m = np.asmatrix(np.mean(r, axis=0)) + # ### + stds = np.std(r, axis=0) + # Compute excess returns matrix (xr) + xr = r - m + # Matrix algebra to get variance-covariance matrix + cov_m = np.dot(np.transpose(xr),xr)/n + # Compute asset correlation matrix (informative only) + corr_m = cov_m/np.dot(np.transpose(stds),stds) + + # Define portfolio optimization parameters + n_portfolios = 50000 + results_array = np.zeros((3+context.nassets,n_portfolios)) + for p in xrange(n_portfolios): + weights = np.random.random(context.nassets) + weights /= np.sum(weights) + w = np.asmatrix(weights) + p_r = np.sum(np.dot(w,np.transpose(m)))*365 + p_std = np.sqrt(np.dot(np.dot(w,cov_m),np.transpose(w)))*np.sqrt(365) + + #store results in results array + results_array[0,p] = p_r + results_array[1,p] = p_std + #store Sharpe Ratio (return / volatility) - risk free rate element + #excluded for simplicity + results_array[2,p] = results_array[0,p] / results_array[1,p] + i = 0 + for iw in weights: + results_array[3+i,p] = weights[i] + i += 1 + + #convert results array to Pandas DataFrame + results_frame = pd.DataFrame(np.transpose(results_array), + columns=['r','stdev','sharpe']+context.assets) + #locate position of portfolio with highest Sharpe Ratio + max_sharpe_port = results_frame.iloc[results_frame['sharpe'].idxmax()] + #locate positon of portfolio with minimum standard deviation + min_vol_port = results_frame.iloc[results_frame['stdev'].idxmin()] + + #order optimal weights for each asset + for asset in context.assets: + if data.can_trade(asset): + order_target_percent(asset, max_sharpe_port[asset]) + + #create scatter plot coloured by Sharpe Ratio + plt.scatter(results_frame.stdev,results_frame.r,c=results_frame.sharpe,cmap='RdYlGn') + plt.xlabel('Volatility') + plt.ylabel('Returns') + plt.colorbar() + #plot red star to highlight position of portfolio with highest Sharpe Ratio + plt.scatter(max_sharpe_port[1],max_sharpe_port[0],marker='o',color='b',s=200) + #plot green star to highlight position of minimum variance portfolio + plt.show() + print(max_sharpe_port) + record(pr=pr,r=r, m=m, stds=stds ,max_sharpe_port=max_sharpe_port, corr_m=corr_m) + context.i += 1 + + +def analyze(context=None, results=None): + # Form DataFrame with selected data + data = results[['pr','r','m','stds','max_sharpe_port','corr_m','portfolio_value']] + + # Save results in CSV file + filename = os.path.splitext(os.path.basename(__file__))[0] + data.to_csv(filename + '.csv') + + +# Bitcoin data is available from 2015-3-2. Dates vary for other tokens. +start = datetime(2017, 1, 1, 0, 0, 0, 0, pytz.utc) +end = datetime(2017, 8, 16, 0, 0, 0, 0, pytz.utc) +results = run_algorithm(initialize=initialize, + handle_data=handle_data, + analyze=analyze, + start=start, + end=end, + exchange_name='poloniex', + capital_base=100000, ) From c8eaa11f801cbd9a07adcd1da13c6517b818fa14 Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Wed, 29 Nov 2017 09:37:46 -0700 Subject: [PATCH 11/11] DOC: added portfolio_optimization to documented examples --- docs/source/example-algos.rst | 152 ++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/docs/source/example-algos.rst b/docs/source/example-algos.rst index 002dc46b..550d8b58 100644 --- a/docs/source/example-algos.rst +++ b/docs/source/example-algos.rst @@ -31,6 +31,13 @@ Overview `two-part video tutorial `_ to show how to get started in backtesting and live trading with Catalyst. +- :ref:`Portfolio Optimization `: Use this code to + execute a portfolio optimization model. This strategy will select the + portfolio with the maximum Sharpe Ratio. The parameters are set to use 180 + days of historical data and rebalance every 30 days. This code was used in + writting the following article: + `Markowitz Portfolio Optimization for Cryptocurrencies `_. + .. _buy_btc_simple: @@ -746,4 +753,149 @@ implemented after the video was recorded, which executes the orders at slighlty different prices, but resulting in significant changes in performance of our strategy. +.. _portfolio_optimization: + +Portfolio Optimization +~~~~~~~~~~~~~~~~~~~~~~ + +Use this code to execute a portfolio optimization model. This strategy will +select the portfolio with the maximum Sharpe Ratio. The parameters are set to +use 180 days of historical data and rebalance every 30 days. This code was used +in writting the following article: +`Markowitz Portfolio Optimization for Cryptocurrencies `_. + +.. code-block:: python + + ''' + You can run this code using the Python interpreter: + + $ python portfolio_optimization.py + ''' + + from __future__ import division + import os + import pytz + import numpy as np + import pandas as pd + from scipy.optimize import minimize + import matplotlib.pyplot as plt + from datetime import datetime + + from catalyst.api import record, symbol, symbols, order_target_percent + from catalyst.utils.run_algo import run_algorithm + + np.set_printoptions(threshold='nan', suppress=True) + + + def initialize(context): + # Portfolio assets list + context.assets = symbols('btc_usdt', 'eth_usdt', 'ltc_usdt', 'dash_usdt', + 'xmr_usdt') + context.nassets = len(context.assets) + # Set the time window that will be used to compute expected return + # and asset correlations + context.window = 180 + # Set the number of days between each portfolio rebalancing + context.rebalance_period = 30 + context.i = 0 + + + def handle_data(context, data): + # Only rebalance at the beggining of the algorithm execution and + # every multiple of the rebalance period + if context.i == 0 or context.i%context.rebalance_period == 0: + n = context.window + prices = data.history(context.assets, fields='price', + bar_count=n+1, frequency='1d') + pr = np.asmatrix(prices) + t_prices = prices.iloc[1:n+1] + t_val = t_prices.values + tminus_prices = prices.iloc[0:n] + tminus_val = tminus_prices.values + # Compute daily returns (r) + r = np.asmatrix(t_val/tminus_val-1) + # Compute the expected returns of each asset with the average + # daily return for the selected time window + m = np.asmatrix(np.mean(r, axis=0)) + # ### + stds = np.std(r, axis=0) + # Compute excess returns matrix (xr) + xr = r - m + # Matrix algebra to get variance-covariance matrix + cov_m = np.dot(np.transpose(xr),xr)/n + # Compute asset correlation matrix (informative only) + corr_m = cov_m/np.dot(np.transpose(stds),stds) + + # Define portfolio optimization parameters + n_portfolios = 50000 + results_array = np.zeros((3+context.nassets,n_portfolios)) + for p in xrange(n_portfolios): + weights = np.random.random(context.nassets) + weights /= np.sum(weights) + w = np.asmatrix(weights) + p_r = np.sum(np.dot(w,np.transpose(m)))*365 + p_std = np.sqrt(np.dot(np.dot(w,cov_m),np.transpose(w)))*np.sqrt(365) + + #store results in results array + results_array[0,p] = p_r + results_array[1,p] = p_std + #store Sharpe Ratio (return / volatility) - risk free rate element + #excluded for simplicity + results_array[2,p] = results_array[0,p] / results_array[1,p] + i = 0 + for iw in weights: + results_array[3+i,p] = weights[i] + i += 1 + + #convert results array to Pandas DataFrame + results_frame = pd.DataFrame(np.transpose(results_array), + columns=['r','stdev','sharpe']+context.assets) + #locate position of portfolio with highest Sharpe Ratio + max_sharpe_port = results_frame.iloc[results_frame['sharpe'].idxmax()] + #locate positon of portfolio with minimum standard deviation + min_vol_port = results_frame.iloc[results_frame['stdev'].idxmin()] + + #order optimal weights for each asset + for asset in context.assets: + if data.can_trade(asset): + order_target_percent(asset, max_sharpe_port[asset]) + + #create scatter plot coloured by Sharpe Ratio + plt.scatter(results_frame.stdev,results_frame.r,c=results_frame.sharpe,cmap='RdYlGn') + plt.xlabel('Volatility') + plt.ylabel('Returns') + plt.colorbar() + #plot red star to highlight position of portfolio with highest Sharpe Ratio + plt.scatter(max_sharpe_port[1],max_sharpe_port[0],marker='o',color='b',s=200) + #plot green star to highlight position of minimum variance portfolio + plt.show() + print(max_sharpe_port) + record(pr=pr,r=r, m=m, stds=stds ,max_sharpe_port=max_sharpe_port, corr_m=corr_m) + context.i += 1 + + + def analyze(context=None, results=None): + # Form DataFrame with selected data + data = results[['pr','r','m','stds','max_sharpe_port','corr_m','portfolio_value']] + + # Save results in CSV file + filename = os.path.splitext(os.path.basename(__file__))[0] + data.to_csv(filename + '.csv') + + + # Bitcoin data is available from 2015-3-2. Dates vary for other tokens. + start = datetime(2017, 1, 1, 0, 0, 0, 0, pytz.utc) + end = datetime(2017, 8, 16, 0, 0, 0, 0, pytz.utc) + results = run_algorithm(initialize=initialize, + handle_data=handle_data, + analyze=analyze, + start=start, + end=end, + exchange_name='poloniex', + capital_base=100000, ) + +.. image:: https://cdn-images-1.medium.com/max/1600/0*EjjiKZHlYF3sn7yQ. + :align: center + +