Compare commits

..
79 Commits
Author SHA1 Message Date
fredfortier 1696db930d Merge branch 'develop' 2017-11-27 17:37:15 -05:00
fredfortier 9397b3fd5a BLD: testing simple universe with Bitfinex 2017-11-27 17:36:44 -05:00
fredfortier 2bb11db412 BLD: modified sample algo for testing 2017-11-27 17:28:13 -05:00
fredfortier c2f3e00d99 BUG: Adding back missing constants 2017-11-27 17:27:43 -05:00
fredfortier 292fe66d3f Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	catalyst/constants.py
2017-11-27 17:17:05 -05:00
fredfortier ba46015bae BLD: completed implementation of issue #65, support for custom exchange data 2017-11-27 17:16:44 -05:00
Victor Grau Serrat 12d5915c8e ENH: DEBUG level can be easily overriden from the local environment 2017-11-27 15:02:13 -07:00
Victor Grau Serrat c6fe45371c ENH: removed default for --capital_base in backtesting 2017-11-27 13:20:00 -07:00
fredfortier 968e70b69b BLD: implementing issue #65, implemented custom exchange data 2017-11-25 07:43:20 -05:00
fredfortier 6a7c47f3a9 BLD: implementing issue #65, adding local symbols definition 2017-11-24 01:58:33 -05:00
fredfortier 7daf295e63 BLD: refactoring to decrease reliance on the Exchange in preparation to support ad-hoc CSV bundles 2017-11-23 22:11:23 -05:00
fredfortier 7dddc0a85f BUG: fixed issue #80 but updated performance stats immediately after registering transactions in live mode 2017-11-22 21:11:16 -05:00
fredfortier 32523d474d Merge remote-tracking branch 'origin/develop' into develop 2017-11-22 16:03:29 -05:00
fredfortier 841acf0203 BLD: implemented issue #79, using the capital_base parameter to override the amount of base currency available for trading 2017-11-22 16:03:21 -05:00
Victor Grau Serrat b4ab1a5375 DOC: remake of beginner tutorial 2017-11-21 22:53:50 -07:00
fredfortier 3ec9853b75 BUG: in relation to issue #77, catching the remaining warnings 2017-11-21 20:42:44 -05:00
fredfortier c1d140a831 BUG: fixed issue #77, a sortino warning prevents analyze() from completing 2017-11-21 15:55:56 -05:00
fredfortier 02dc4d6a30 BUG: made some live-trading adjustments related to issue #71 2017-11-21 13:56:05 -05:00
fredfortier 0d366a350d BUG: fixed #75, adjusted the ruturn value of run_algorithm to support minute stats. 2017-11-20 21:53:48 -05:00
fredfortier 1e8b0c36a1 BUG: fixed #74, a problematic scenario when retrieving the history of multiple assets. 2017-11-20 20:00:48 -05:00
fredfortier 0af592a5f4 BUG: fixed issue #71 with the last candle of a resampled set 2017-11-20 17:52:29 -05:00
Victor Grau Serrat 86b2a5c772 DOC: videos: +3rd_install, +backtest 2017-11-20 09:31:47 -07:00
Victor Grau Serrat 9cfd50dc4f DOC: mean_reversion_simple.py minor edits, and added to doc website 2017-11-20 09:12:43 -07:00
Victor Grau Serrat 698b19c8fa DOC: updated examples/buy_and_hodl.py. Added Example Algos and Utilities pages to the documentation 2017-11-19 23:51:57 -07:00
Victor Grau Serrat 5d4bc99097 Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-11-19 22:04:28 -07:00
Victor Grau Serrat cfb3f1ca42 DOC: restructured install page 2017-11-19 22:04:11 -07:00
fredfortier ee1605a5e6 Merge branch 'abnera-patch-2' into develop 2017-11-17 19:43:46 -05:00
fredfortier f3dca74e87 BUG: fixed a get_candles issue with the Poloniex exchange 2017-11-17 19:40:49 -05:00
Victor Grau Serrat d57b79427b BUG: enforced --capital base in backtesting 2017-11-17 10:43:20 -07:00
Victor Grau Serrat 8a89c0c53f BUG: enforced --base_currency in backtesting. Fixes #67. 2017-11-17 09:52:25 -07:00
fredfortier c260e188b0 BUG: looking a potential resampling issue 2017-11-16 18:14:24 -05:00
fredfortier 230b9c17eb Merge branch 'patch-2' of https://github.com/abnera/catalyst into abnera-patch-2 2017-11-16 16:58:15 -05:00
fredfortier 2a8b5cf911 Merge branch 'damo1884-talib_example' into develop 2017-11-16 16:56:07 -05:00
fredfortier 3fa88a3e56 BLD: misc housekeeping 2017-11-16 16:55:40 -05:00
fredfortier 64532c3d08 BLD: minor adjustments to the talib sample algo 2017-11-16 16:54:32 -05:00
fredfortier 5f86ab659e Merge branch 'talib_example' of https://github.com/damo1884/catalyst into damo1884-talib_example 2017-11-16 16:48:10 -05:00
Abner Ayala-AcevedoandGitHub df14a94918 Modified for examples consistency.
Fully tested on v0.3.8
2017-11-16 11:22:35 -08:00
fredfortier e087e48088 BLD: polishing a sample algorithm 2017-11-14 17:04:38 -05:00
fredfortier 5110b37a82 Merge remote-tracking branch 'origin/develop' into develop 2017-11-14 16:39:19 -05:00
Victor Grau Serrat a2bb231424 DOC: improving Win/Conda install instructions 2017-11-14 12:14:38 -07:00
fredfortier e939f742a8 Merge branch 'master' into develop 2017-11-14 13:59:17 -05:00
fredfortier 9093be748e BUG: fixed a warning filter issue 2017-11-14 13:58:24 -05:00
fredfortier e23a7e67a0 merging from the develop branch 2017-11-14 13:29:50 -05:00
fredfortier 273b4fb7a7 DOC: updated release notes 2017-11-14 13:27:20 -05:00
fredfortier 0b2684d532 BLD: polishing a sample algorithm 2017-11-14 13:22:27 -05:00
fredfortier 224192a1ee BUG: fixed issue #63 warnings with cumulative metrics 2017-11-14 11:50:38 -05:00
fredfortier 2d7202ac81 BUG: fixed issue #64 with SSL certificates 2017-11-14 11:40:11 -05:00
fredfortier 8d95428fa6 BLD: created a simpler mean-reversion algo for the video 2017-11-14 11:01:46 -05:00
fredfortier d678376d8d BLD: polishing a sample algorithm 2017-11-14 02:28:22 -05:00
fredfortier 9b5fa83da3 BLD: polishing a sample algorithm 2017-11-14 01:00:21 -05:00
fredfortier 0f1c3e1ace Merge remote-tracking branch 'origin/develop' into develop 2017-11-13 22:06:21 -05:00
fredfortier f3cb610748 BLD: polishing a sample algorithm 2017-11-13 22:06:06 -05:00
Victor Grau Serrat 1e6316d414 BUG: PoloniexCurator: connection retries when fetching data 2017-11-13 15:31:49 -07:00
fredfortier df51cbe21b Merge remote-tracking branch 'origin/develop' into develop 2017-11-13 16:57:46 -05:00
fredfortier dce31b212b BLD: polishing a sample algorithm 2017-11-13 16:57:37 -05:00
Victor Grau Serrat 00269d3dfb MAINT: PoloniexCurator PEP8 edits 2017-11-13 14:41:10 -07:00
fredfortier 648be3969a DOC: added release notes of upcoming 0.3.7 release 2017-11-11 18:09:24 -05:00
fredfortier a54325fdcf BLD: issue #62, the stats now align with the data_frequency selected in the algo 2017-11-10 19:59:42 -05:00
fredfortier b64e5929b4 BUG: resolve issue #61 by adjusting our perf conventions to match zipline exactly. 2017-11-10 17:39:36 -05:00
fredfortier 631cbcd352 BLD: Working on the sample algo for intro videos. Made auto-ingestion configurable. 2017-11-09 19:56:57 -05:00
fredfortier 24c5a5bd13 Merge remote-tracking branch 'origin/develop' into develop 2017-11-09 17:10:06 -05:00
fredfortier 1103947af0 BLD: created a new sample algo for instructional materials. Fixed some minor issues in the process. 2017-11-09 17:09:55 -05:00
damo1884 061de3c12f fix issue with candlestick chart 2017-11-08 19:28:44 -08:00
lacabra 207887a28d MAINT: PoloniexCurator cleanup 2017-11-08 20:43:40 +00:00
Victor Grau Serrat dc53f973e4 Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-11-08 11:32:00 -07:00
Victor Grau Serrat 9a80a488cd MAINT: handful of coins with first tradeID > 1 and PEP8 2017-11-08 11:31:56 -07:00
fredfortier a85b6c798a BUG: fixes related to issue #47 and added a verbose ingestion option. 2017-11-07 13:42:47 -05:00
Victor Grau Serrat 9229809b05 DOC: fix broken documentation 2017-11-06 16:41:04 -07:00
fredfortier f81cf6b600 BUG: working on issue #57 2017-11-06 15:07:24 -05:00
fredfortier ba4ffc7272 BUG: working on issue #57 2017-11-06 15:04:44 -05:00
damo1884 12695474e3 Add TALib Simple Example 2017-11-05 01:14:09 -07:00
fredfortier 9d1dd5829d Merge branch 'develop' 2017-11-04 16:48:59 -04:00
fredfortier d4148891fc DOC: updated release notes prior to release 2017-11-04 16:43:27 -04:00
fredfortier c9c16f54b1 BUG: fixed on issue #55 with single history bar 2017-11-04 16:38:54 -04:00
fredfortier 7da72fe9cb BUG: working on issue #55 2017-11-04 16:22:10 -04:00
Abner Ayala-AcevedoandGitHub cd0157347f Refactoring 2017-10-26 18:10:12 -07:00
Abner Ayala-AcevedoandGitHub 76a8362e3d Update simple_universe.py 2017-10-26 15:17:02 -07:00
Abner Ayala-AcevedoandGitHub c8cc2edd36 Convert to 30 minutes ohlcv data 2017-10-26 15:16:13 -07:00
Abner Ayala-AcevedoandGitHub cb870422c3 Create simple_universe.py
This example aims to help users get familiar with catalyst API's to collect and handle data.
2017-10-25 12:11:40 -07:00
45 changed files with 4137 additions and 987 deletions
+72 -25
View File
@@ -9,6 +9,7 @@ from six import text_type
from catalyst.data import bundles as bundles_module from catalyst.data import bundles as bundles_module
from catalyst.exchange.exchange_bundle import ExchangeBundle from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.exchange_utils import delete_algo_folder
from catalyst.exchange.factory import get_exchange from catalyst.exchange.factory import get_exchange
from catalyst.utils.cli import Date, Timestamp from catalyst.utils.cli import Date, Timestamp
from catalyst.utils.run_algo import _run, load_extensions from catalyst.utils.run_algo import _run, load_extensions
@@ -29,16 +30,17 @@ except NameError:
@click.option( @click.option(
'--strict-extensions/--non-strict-extensions', '--strict-extensions/--non-strict-extensions',
is_flag=True, is_flag=True,
help='If --strict-extensions is passed then catalyst will not run if it' help='If --strict-extensions is passed then catalyst will not run '
' cannot load all of the specified extensions. If this is not passed or' 'if it cannot load all of the specified extensions. If this is '
' --non-strict-extensions is passed then the failure will be logged but' 'not passed or --non-strict-extensions is passed then the '
' execution will continue.', 'failure will be logged but execution will continue.',
) )
@click.option( @click.option(
'--default-extension/--no-default-extension', '--default-extension/--no-default-extension',
is_flag=True, is_flag=True,
default=True, default=True,
help="Don't load the default catalyst extension.py file in $CATALYST_HOME.", help="Don't load the default catalyst extension.py file "
"in $CATALYST_HOME.",
) )
@click.version_option() @click.version_option()
def main(extension, strict_extensions, default_extension): def main(extension, strict_extensions, default_extension):
@@ -123,9 +125,9 @@ def ipython_only(option):
'--define', '--define',
multiple=True, multiple=True,
help="Define a name to be bound in the namespace before executing" help="Define a name to be bound in the namespace before executing"
" the algotext. For example '-Dname=value'. The value may be any python" " the algotext. For example '-Dname=value'. The value may be"
" expression. These are evaluated in order so they may refer to previously" " any python expression. These are evaluated in order so they"
" defined names.", " may refer to previously defined names.",
) )
@click.option( @click.option(
'--data-frequency', '--data-frequency',
@@ -137,7 +139,6 @@ def ipython_only(option):
@click.option( @click.option(
'--capital-base', '--capital-base',
type=float, type=float,
default=10e6,
show_default=True, show_default=True,
help='The starting capital for the simulation.', help='The starting capital for the simulation.',
) )
@@ -175,8 +176,8 @@ def ipython_only(option):
default='-', default='-',
metavar='FILENAME', metavar='FILENAME',
show_default=True, show_default=True,
help="The location to write the perf data. If this is '-' the perf will" help="The location to write the perf data. If this is '-' the perf"
" be written to stdout.", " will be written to stdout.",
) )
@click.option( @click.option(
'--print-algo/--no-print-algo', '--print-algo/--no-print-algo',
@@ -194,7 +195,8 @@ def ipython_only(option):
'-x', '-x',
'--exchange-name', '--exchange-name',
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}), type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
help='The name of the targeted exchange (supported: bitfinex, bittrex, poloniex).', help='The name of the targeted exchange (supported: bitfinex,'
' bittrex, poloniex).',
) )
@click.option( @click.option(
'-n', '-n',
@@ -239,16 +241,26 @@ def run(ctx,
# does not pass either of these and then passes the first only # does not pass either of these and then passes the first only
# to be told they need to pass the second argument also # to be told they need to pass the second argument also
ctx.fail( ctx.fail(
"must specify dates with '-s' / '--start' and '-e' / '--end'", "must specify dates with '-s' / '--start' and '-e' / '--end'"
" in backtest mode",
) )
if start is None: if start is None:
ctx.fail("must specify a start date with '-s' / '--start'") ctx.fail("must specify a start date with '-s' / '--start'"
" in backtest mode")
if end is None: if end is None:
ctx.fail("must specify an end date with '-e' / '--end'") ctx.fail("must specify an end date with '-e' / '--end'"
" in backtest mode")
if exchange_name is None: if exchange_name is None:
ctx.fail("must specify an exchange name '-x'") ctx.fail("must specify an exchange name '-x'")
if base_currency is None:
ctx.fail("must specify a base currency with '-c' in backtest mode")
if capital_base is None:
ctx.fail("must specify a capital base with '--capital-base'"
" in backtest mode")
perf = _run( perf = _run(
initialize=None, initialize=None,
handle_data=None, handle_data=None,
@@ -334,9 +346,9 @@ def catalyst_magic(line, cell=None):
'--define', '--define',
multiple=True, multiple=True,
help="Define a name to be bound in the namespace before executing" help="Define a name to be bound in the namespace before executing"
" the algotext. For example '-Dname=value'. The value may be any python" " the algotext. For example '-Dname=value'. The value may be"
" expression. These are evaluated in order so they may refer to previously" " any python expression. These are evaluated in order so they"
" defined names.", " may refer to previously defined names.",
) )
@click.option( @click.option(
'-o', '-o',
@@ -363,7 +375,8 @@ def catalyst_magic(line, cell=None):
'-x', '-x',
'--exchange-name', '--exchange-name',
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}), type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
help='The name of the targeted exchange (supported: bitfinex, bittrex, poloniex).', help='The name of the targeted exchange (supported: bitfinex,'
' bittrex, poloniex).',
) )
@click.option( @click.option(
'-n', '-n',
@@ -485,13 +498,31 @@ def live(ctx,
help='A list of symbols to exclude from the ingestion ' help='A list of symbols to exclude from the ingestion '
'(optional comma separated list)', '(optional comma separated list)',
) )
@click.option(
'--csv',
default=None,
help='The path of a CSV file containing the data. If specified, start, '
'end, include-symbols and exclude-symbols will be ignored. Instead,'
'all data in the file will be ingested.',
)
@click.option( @click.option(
'--show-progress/--no-show-progress', '--show-progress/--no-show-progress',
default=True, default=True,
help='Print progress information to the terminal.' help='Print progress information to the terminal.'
) )
@click.option(
'--verbose/--no-verbose`',
default=False,
help='Show a progress indicator for every currency pair.'
)
@click.option(
'--validate/--no-validate`',
default=False,
help='Report potential anomalies found in data bundles.'
)
def ingest_exchange(exchange_name, data_frequency, start, end, def ingest_exchange(exchange_name, data_frequency, start, end,
include_symbols, exclude_symbols, show_progress): include_symbols, exclude_symbols, csv, show_progress,
verbose, validate):
""" """
Ingest data for the given exchange. Ingest data for the given exchange.
""" """
@@ -499,8 +530,7 @@ def ingest_exchange(exchange_name, data_frequency, start, end,
if exchange_name is None: if exchange_name is None:
ctx.fail("must specify an exchange name '-x'") ctx.fail("must specify an exchange name '-x'")
exchange = get_exchange(exchange_name) exchange_bundle = ExchangeBundle(exchange_name)
exchange_bundle = ExchangeBundle(exchange)
click.echo('Ingesting exchange bundle {}...'.format(exchange_name)) click.echo('Ingesting exchange bundle {}...'.format(exchange_name))
exchange_bundle.ingest( exchange_bundle.ingest(
@@ -509,10 +539,28 @@ def ingest_exchange(exchange_name, data_frequency, start, end,
exclude_symbols=exclude_symbols, exclude_symbols=exclude_symbols,
start=start, start=start,
end=end, end=end,
show_progress=show_progress show_progress=show_progress,
show_breakdown=verbose,
show_report=validate,
csv=csv
) )
@main.command(name='clean-algo')
@click.option(
'-n',
'--algo-namespace',
help='The label of the algorithm to for which to clean the state.'
)
@click.pass_context
def clean_algo(ctx, algo_namespace):
click.echo(
'Cleaning algo state: {}'.format(algo_namespace)
)
delete_algo_folder(algo_namespace)
click.echo('Done')
@main.command(name='clean-exchange') @main.command(name='clean-exchange')
@click.option( @click.option(
'-x', '-x',
@@ -537,8 +585,7 @@ def clean_exchange(ctx, exchange_name, data_frequency):
if exchange_name is None: if exchange_name is None:
ctx.fail("must specify an exchange name '-x'") ctx.fail("must specify an exchange name '-x'")
exchange = get_exchange(exchange_name) exchange_bundle = ExchangeBundle(exchange_name)
exchange_bundle = ExchangeBundle(exchange)
click.echo('Cleaning exchange bundle {}...'.format(exchange_name)) click.echo('Cleaning exchange bundle {}...'.format(exchange_name))
exchange_bundle.clean( exchange_bundle.clean(
+13 -5
View File
@@ -38,6 +38,7 @@ from numpy cimport int64_t
import warnings import warnings
cimport numpy as np cimport numpy as np
from catalyst.exchange.exchange_utils import get_sid
from catalyst.utils.calendars import get_calendar from catalyst.utils.calendars import get_calendar
from catalyst.exchange.exchange_errors import InvalidSymbolError, SidHashError from catalyst.exchange.exchange_errors import InvalidSymbolError, SidHashError
@@ -503,11 +504,7 @@ cdef class TradingPair(Asset):
if sid == 0 or sid is None: if sid == 0 or sid is None:
try: try:
# sid = abs(hash(symbol)) % (10 ** 4) sid = get_sid(symbol)
# TODO: try to encode the symbol in the main scope
sid = int(
hashlib.sha256(symbol.encode('utf-8')).hexdigest(), 16
) % 10 ** 6
except Exception as e: except Exception as e:
raise SidHashError(symbol=symbol) raise SidHashError(symbol=symbol)
@@ -559,6 +556,17 @@ cdef class TradingPair(Asset):
end_minute=self.end_minute end_minute=self.end_minute
) )
cpdef to_dict(self):
"""
Convert to a python dict.
"""
super_dict = super(TradingPair, self).to_dict()
super_dict['end_daily'] = self.end_daily
super_dict['end_minute'] = self.end_minute
super_dict['leverage'] = self.leverage
super_dict['min_trade_size'] = self.min_trade_size
return super_dict
def is_exchange_open(self, dt_minute): def is_exchange_open(self, dt_minute):
""" """
Parameters Parameters
+14 -1
View File
@@ -1,5 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import logbook import logbook
LOG_LEVEL = logbook.INFO ''' You can override the LOG level from your environment.
For example, if you want to see the DEBUG messages, run:
$ export CATALYST_LOG_LEVEL=10
'''
LOG_LEVEL = int(os.environ.get('CATALYST_LOG_LEVEL', logbook.INFO))
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
'{exchange}/symbols.json'
DATE_TIME_FORMAT = '%Y-%m-%d %H:%M'
DATE_FORMAT = '%Y-%m-%d'
AUTO_INGEST = False
+198 -116
View File
@@ -6,9 +6,8 @@ from catalyst.exchange.exchange_utils import get_exchange_symbols_filename
DT_START = int(time.mktime(datetime(2010, 1, 1, 0, 0).timetuple())) DT_START = int(time.mktime(datetime(2010, 1, 1, 0, 0).timetuple()))
DT_END = int(time.time()) DT_END = pd.to_datetime('today').value // 10 ** 9
CSV_OUT_FOLDER = '/var/tmp/catalyst/data/poloniex/' CSV_OUT_FOLDER = os.environ.get('CSV_OUT_FOLDER', '/efs/exchanges/poloniex/')
CSV_OUT_FOLDER = '/Volumes/enigma/data/poloniex/'
CONN_RETRIES = 2 CONN_RETRIES = 2
logbook.StderrHandler().push_application() logbook.StderrHandler().push_application()
@@ -27,13 +26,15 @@ class PoloniexCurator(object):
try: try:
os.makedirs(CSV_OUT_FOLDER) os.makedirs(CSV_OUT_FOLDER)
except Exception as e: except Exception as e:
log.error('Failed to create data folder: %s' % CSV_OUT_FOLDER) log.error('Failed to create data folder: {}'.format(
CSV_OUT_FOLDER))
log.exception(e) log.exception(e)
'''
Retrieves and returns all currency pairs from the exchange
'''
def get_currency_pairs(self): def get_currency_pairs(self):
'''
Retrieves and returns all currency pairs from the exchange
'''
url = self._api_path + 'command=returnTicker' url = self._api_path + 'command=returnTicker'
try: try:
@@ -49,89 +50,136 @@ class PoloniexCurator(object):
self.currency_pairs.append(ticker) self.currency_pairs.append(ticker)
self.currency_pairs.sort() self.currency_pairs.sort()
log.debug('Currency pairs retrieved successfully: %d' % (len(self.currency_pairs))) log.debug('Currency pairs retrieved successfully: {}'.format(
len(self.currency_pairs)
))
'''
Helper function that reads tradeID and date fields from CSV readline
'''
def _retrieve_tradeID_date(self, row): def _retrieve_tradeID_date(self, row):
'''
Helper function that reads tradeID and date fields from CSV readline
'''
tId = int(row.split(',')[0]) tId = int(row.split(',')[0])
d = pd.to_datetime( row.split(',')[1], infer_datetime_format=True).value // 10 ** 9 d = pd.to_datetime(row.split(',')[1],
infer_datetime_format=True).value // 10 ** 9
return tId, d return tId, d
'''
Retrieves TradeHistory from exchange for a given currencyPair between start and end dates. def retrieve_trade_history(self, currencyPair, start=DT_START,
If no start date is provided, uses a system-wide one (beginning of time for cryptotrading) end=DT_END, temp=None):
If no end date is provided, 'now' is used '''
Retrieves TradeHistory from exchange for a given currencyPair
between start and end dates. If no start date is provided, uses
a system-wide one (beginning of time for cryptotrading).
If no end date is provided, 'now' is used.
Stores results in CSV file on disk. Stores results in CSV file on disk.
This function is called recursively to work around the limitations imposed by the provider API.
''' This function is called recursively to work around the
def retrieve_trade_history(self, currencyPair, start=DT_START, end=DT_END, temp=None): limitations imposed by the provider API.
'''
csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv' csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
''' '''
Check what data we already have on disk, reading first and last lines from file. Check what data we already have on disk, reading first and last
Data is stored on file from NEWEST to OLDEST. lines from file. Data is stored on file from NEWEST to OLDEST.
''' '''
try: try:
with open(csv_fn, 'ab+') as f: with open(csv_fn, 'ab+') as f:
f.seek(0, os.SEEK_END) f.seek(0, os.SEEK_END)
if(f.tell() > 2): # First check file is not zero size if(f.tell() > 2): # Check file size is not 0
f.seek(0) # Go to the beginning to read first line f.seek(0) # Go to start to read
last_tradeID, end_file = self._retrieve_tradeID_date(f.readline()) last_tradeID, end_file = self._retrieve_tradeID_date(f.readline())
f.seek(-2, os.SEEK_END) # Jump to the second last byte. f.seek(-2, os.SEEK_END) # Jump to the 2nd last byte
while f.read(1) != b"\n": # Until EOL is found... while f.read(1) != b"\n": # Until EOL is found...
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more. f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
first_tradeID, start_file = self._retrieve_tradeID_date(f.readline()) first_tradeID, start_file = self._retrieve_tradeID_date(f.readline())
if( first_tradeID == 1 and end_file + 3600 > DT_END ): if( end_file + 3600 * 6 > DT_END and ( first_tradeID == 1
or (currencyPair == 'BTC_HUC' and first_tradeID == 2)
or (currencyPair == 'BTC_RIC' and first_tradeID == 2)
or (currencyPair == 'BTC_XCP' and first_tradeID == 2)
or (currencyPair == 'BTC_NAV' and first_tradeID == 4569)
or (currencyPair == 'BTC_POT' and first_tradeID == 23511) ) ):
return return
except Exception as e: except Exception as e:
log.error('Error opening file: %s' % csv_fn) log.error('Error opening file: {}'.format(csv_fn))
log.exception(e) log.exception(e)
''' '''
Poloniex API limits querying TradeHistory to intervals smaller than 1 month, Poloniex API limits querying TradeHistory to intervals smaller
so we make sure that start date is never more than 1 month apart from end date than 1 month, so we make sure that start date is never more than
1 month apart from end date
''' '''
if( end - start > 2419200 ): # 60 s/min * 60 min/hr * 24 hr/day * 28 days if( end - start > 2419200 ): # 60s/min * 60min/hr * 24hr/day * 28days
newstart = end - 2419200 newstart = end - 2419200
else: else:
newstart = start newstart = start
log.debug(currencyPair+': Retrieving from '+str(newstart)+' to '+str(end) +'\t ' log.debug('{}: Retrieving from {} to {}\t {} - {}'.format(
+ time.ctime(newstart) + ' - '+ time.ctime(end)) currencyPair, str(newstart), str(end),
time.ctime(newstart), time.ctime(end)))
url = self._api_path + 'command=returnTradeHistory&currencyPair=' + currencyPair + '&start=' + str(newstart) + '&end=' + str(end) url = '{path}command=returnTradeHistory&currencyPair={pair}' \
'&start={start}&end={end}'.format(
path = self._api_path,
pair = currencyPair,
start = str(newstart),
end = str(end)
)
print url
try: attempts = 0
response = requests.get(url) success = 0
except Exception as e: while attempts < CONN_RETRIES:
log.error('Failed to retrieve trade history data for %s' % currencyPair) try:
log.exception(e) response = requests.get(url)
except Exception as e:
log.error('Failed to retrieve trade history data for {}'.format(
currencyPair
))
log.exception(e)
attempts += 1
else:
try:
if isinstance(response.json(), dict) and response.json()['error']:
log.error('Failed to to retrieve trade history data '
'for {}: {}'.format(
currencyPair,
response.json()['error']
))
attempts += 1
except Exception as e:
log.exception(e)
attempts += 1
else:
success = 1
break
if not success:
return None return None
else:
if isinstance(response.json(), dict) and response.json()['error']:
log.error('Failed to to retrieve trade history data for %s: %s' % (currencyPair,response.json()['error']))
exit(1)
''' '''
If we get to transactionId == 1, and we already have that on disk, If we get to transactionId == 1, and we already have that on
we got to the end of TradeHistory for this coin. disk, we got to the end of TradeHistory for this coin.
''' '''
if('first_tradeID' in locals() and response.json()[-1]['tradeID'] == first_tradeID): if('first_tradeID' in locals()
and response.json()[-1]['tradeID'] == first_tradeID):
return return
''' '''
There are primarily two scenarios: There are primarily two scenarios:
a) There is newer data available that we need to add at the beginning a) There is newer data available that we need to add at
of the file. We'll retrieve all what we need until we get to what the beginning of the file. We'll retrieve all what we
we already have, writing it to a temporary file; and we will write need until we get to what we already have, writing it
that at the beginning of our existing file. to a temporary file; and we will write that at the
b) We are going back in time, appending at the end of our existing beginning of our existing file.
TradeHistory until the first transaction for this currencyPair b) We are going back in time, appending at the end of
our existing TradeHistory until the first transaction
for this currencyPair
''' '''
try: try:
if( 'end_file' in locals() and end_file + 3600 < end): if( 'end_file' in locals() and end_file + 3600 < end):
@@ -151,8 +199,10 @@ class PoloniexCurator(object):
item['globalTradeID'] item['globalTradeID']
]) ])
if( response.json()[-1]['tradeID'] > last_tradeID ): if( response.json()[-1]['tradeID'] > last_tradeID ):
end = pd.to_datetime( response.json()[-1]['date'], infer_datetime_format=True).value // 10 ** 9 end = pd.to_datetime( response.json()[-1]['date'],
self.retrieve_trade_history(currencyPair, start, end, temp=temp) infer_datetime_format=True).value // 10 ** 9
self.retrieve_trade_history(currencyPair, start,
end, temp=temp)
else: else:
with open(csv_fn,'rb+') as f: with open(csv_fn,'rb+') as f:
shutil.copyfileobj(f,temp) shutil.copyfileobj(f,temp)
@@ -165,7 +215,8 @@ class PoloniexCurator(object):
with open(csv_fn, 'ab') as csvfile: with open(csv_fn, 'ab') as csvfile:
csvwriter = csv.writer(csvfile) csvwriter = csv.writer(csvfile)
for item in response.json(): for item in response.json():
if( 'first_tradeID' in locals() and item['tradeID'] >= first_tradeID ): if( 'first_tradeID' in locals()
and item['tradeID'] >= first_tradeID ):
continue continue
csvwriter.writerow([ csvwriter.writerow([
item['tradeID'], item['tradeID'],
@@ -176,84 +227,112 @@ class PoloniexCurator(object):
item['total'], item['total'],
item['globalTradeID'] item['globalTradeID']
]) ])
end = pd.to_datetime( response.json()[-1]['date'], infer_datetime_format=True).value // 10 ** 9 end = pd.to_datetime(response.json()[-1]['date'],
infer_datetime_format=True).value // 10 ** 9
except Exception as e: except Exception as e:
log.error('Error opening %s' % csv_fn) log.error('Error opening {}'.format(csv_fn))
log.exception(e) log.exception(e)
''' '''
If we got here, we aren't done yet. Call recursively with 'end' times If we got here, we aren't done yet. Call recursively with
that go sequentially back in time. 'end' times that go sequentially back in time.
''' '''
self.retrieve_trade_history(currencyPair, start, end) self.retrieve_trade_history(currencyPair, start, end)
'''
def generate_ohlcv(self, df):
'''
Generates OHLCV dataframe from a dataframe containing all TradeHistory Generates OHLCV dataframe from a dataframe containing all TradeHistory
by resampling with 1-minute period by resampling with 1-minute period
''' '''
def generate_ohlcv(self, df): df.set_index('date', inplace=True) # Index by date
df.set_index('date', inplace=True) # Index by date vol = df['total'].to_frame('volume') # set Vol aside
vol = df['total'].to_frame('volume') # Will deal with vol separately, as ohlc() messes it up df.drop('total', axis=1, inplace=True) # Drop volume data
df.drop('total', axis=1, inplace=True) # Drop volume data from dataframe ohlc = df.resample('T').ohlc() # Resample OHLC 1min
ohlc = df.resample('T').ohlc() # Resample OHLC in 1min bins ohlc.columns = ohlc.columns.map(lambda t: t[1]) # Raname columns by dropping 'rate'
ohlc.columns = ohlc.columns.map(lambda t: t[1]) # Raname columns by dropping 'rate' closes = ohlc['close'].fillna(method='pad') # Pad fwd missing 'close'
closes = ohlc['close'].fillna(method='pad') # Pad forward missing 'close' ohlc = ohlc.apply(lambda x: x.fillna(closes)) # Fill N/A with last close
ohlc = ohlc.apply(lambda x: x.fillna(closes)) # Fill N/A with last close vol = vol.resample('T').sum().fillna(0) # Add volumes by bin
vol = vol.resample('T').sum().fillna(0) # Add volumes by bin ohlcv = pd.concat([ohlc,vol], axis=1) # Concatenate OHLC + Vol
ohlcv = pd.concat([ohlc,vol], axis=1) # Concatenate OHLC + Volume
return ohlcv return ohlcv
'''
Generates OHLCV data file with 1minute bars from TradeHistory on disk
'''
def write_ohlcv_file(self, currencyPair): def write_ohlcv_file(self, currencyPair):
'''
Generates OHLCV data file with 1minute bars from TradeHistory on disk
'''
csv_trades = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv' csv_trades = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
csv_1min = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv' csv_1min = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
#if( os.path.isfile(csv_1min) ): if( os.path.getmtime(csv_1min) > time.time() - 7200 ):
# log.debug(currencyPair+': 1min data already present. Delete the file if you want to rebuild it.') log.debug(currencyPair+': 1min data file already up to date. '
#else: 'Delete the file if you want to rebuild it.')
df = pd.read_csv(csv_trades, names=['tradeID','date','type','rate','amount','total','globalTradeID'], else:
dtype = {'tradeID': int, 'date': str, 'type': str, 'rate': float, 'amount': float, 'total': float, 'globalTradeID': int } ) df = pd.read_csv(csv_trades,
df.drop(['tradeID','type','amount','globalTradeID'], axis=1, inplace=True) names=['tradeID',
df['date'] = pd.to_datetime(df['date'], infer_datetime_format=True) 'date',
ohlcv = self.generate_ohlcv(df) 'type',
try: 'rate',
with open(csv_1min, 'w') as csvfile: 'amount',
csvwriter = csv.writer(csvfile) 'total',
for item in ohlcv.itertuples(): 'globalTradeID'],
if item.Index == 0: dtype = {'tradeID': int,
continue 'date': str,
csvwriter.writerow([ 'type': str,
item.Index.value // 10 ** 9, 'rate': float,
item.open, 'amount': float,
item.high, 'total': float,
item.low, 'globalTradeID': int }
item.close, )
item.volume, df.drop(['tradeID','type','amount','globalTradeID'],
]) axis=1, inplace=True)
except Exception as e: df['date'] = pd.to_datetime(df['date'], infer_datetime_format=True)
log.error('Error opening %s' % csv_fn) ohlcv = self.generate_ohlcv(df)
log.exception(e) try:
log.debug(currencyPair+': Generated 1min OHLCV data.') with open(csv_1min, 'w') as csvfile:
csvwriter = csv.writer(csvfile)
for item in ohlcv.itertuples():
if item.Index == 0:
continue
csvwriter.writerow([
item.Index.value // 10 ** 9,
item.open,
item.high,
item.low,
item.close,
item.volume,
])
except Exception as e:
log.error('Error opening {}'.format(csv_fn))
log.exception(e)
log.debug('{}: Generated 1min OHLCV data.'.format(currencyPair))
'''
Returns a data frame for a given currencyPair from data on disk
'''
def onemin_to_dataframe(self, currencyPair, start, end): def onemin_to_dataframe(self, currencyPair, start, end):
'''
Returns a data frame for a given currencyPair from data on disk
'''
csv_fn = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv' csv_fn = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
df = pd.read_csv(csv_fn, names=['date', 'open', 'high', 'low', 'close', 'volume']) df = pd.read_csv(csv_fn, names=['date',
'open',
'high',
'low',
'close',
'volume']
)
df['date'] = pd.to_datetime(df['date'],unit='s') df['date'] = pd.to_datetime(df['date'],unit='s')
df.set_index('date', inplace=True) df.set_index('date', inplace=True)
return df[start : end] return df[start : end]
'''
Generates a symbols.json file with corresponding start_date for each currencyPair
'''
def generate_symbols_json(self, filename=None): def generate_symbols_json(self, filename=None):
'''
Generates a symbols.json file with corresponding start_date
for each currencyPair
'''
symbol_map = {} symbol_map = {}
if(filename is None): if(filename is None):
@@ -262,14 +341,16 @@ class PoloniexCurator(object):
with open(filename, 'w') as symbols: with open(filename, 'w') as symbols:
for currencyPair in self.currency_pairs: for currencyPair in self.currency_pairs:
start = None start = None
csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv' csv_fn = '{}crypto_trades-{}.csv'.format(
CSV_OUT_FOLDER, currencyPair)
with open(csv_fn, 'r') as f: with open(csv_fn, 'r') as f:
f.seek(0, os.SEEK_END) f.seek(0, os.SEEK_END)
if(f.tell() > 2): # First check file is not zero size if(f.tell() > 2): # Check file size is not 0
f.seek(-2, os.SEEK_END) # Jump to the second last byte. f.seek(-2, os.SEEK_END) # Jump to 2nd last byte
while f.read(1) != b"\n": # Until EOL is found... while f.read(1) != b"\n": # Until EOL is found...
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more. f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
start = pd.to_datetime( f.readline().split(',')[1], infer_datetime_format=True) start = pd.to_datetime( f.readline().split(',')[1],
infer_datetime_format=True)
if(start is None): if(start is None):
start = time.gmtime() start = time.gmtime()
@@ -279,7 +360,8 @@ class PoloniexCurator(object):
symbol = symbol, symbol = symbol,
start_date = start.strftime("%Y-%m-%d") start_date = start.strftime("%Y-%m-%d")
) )
json.dump(symbol_map, symbols, sort_keys=True, indent=2, separators=(',',':')) json.dump(symbol_map, symbols, sort_keys=True, indent=2,
separators=(',',':'))
if __name__ == '__main__': if __name__ == '__main__':
@@ -289,6 +371,6 @@ if __name__ == '__main__':
for currencyPair in pc.currency_pairs: for currencyPair in pc.currency_pairs:
pc.retrieve_trade_history(currencyPair) pc.retrieve_trade_history(currencyPair)
log.debug('{} up to date.'.format(currencyPair))
pc.write_ohlcv_file(currencyPair) pc.write_ohlcv_file(currencyPair)
+3 -2
View File
@@ -149,13 +149,14 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
# exchange.get_history_window() already ensures that we have the right data # exchange.get_history_window() already ensures that we have the right data
# for the right dates # for the right dates
br = exchange.get_history_window( br = exchange.get_history_window_with_bundle(
assets=[benchmark_asset], assets=[benchmark_asset],
end_dt=last_date, end_dt=last_date,
bar_count=pd.Timedelta(last_date - start_dt).days, bar_count=pd.Timedelta(last_date - start_dt).days,
frequency='1d', frequency='1d',
field='close', field='close',
data_frequency='daily') data_frequency='daily',
force_auto_ingest=True)
br.columns = ['close'] br.columns = ['close']
br = br.pct_change(1).iloc[1:] br = br.pct_change(1).iloc[1:]
br.loc[start_dt] = 0 br.loc[start_dt] = 0
+10 -10
View File
@@ -14,6 +14,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import pandas as pd
from catalyst.api import ( from catalyst.api import (
order_target_value, order_target_value,
@@ -23,21 +24,18 @@ from catalyst.api import (
get_open_orders, get_open_orders,
) )
def initialize(context): def initialize(context):
context.ASSET_NAME = 'BTC_USDT' context.ASSET_NAME = 'btc_usdt'
context.TARGET_HODL_RATIO = 0.8 context.TARGET_HODL_RATIO = 0.8
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
# For all trading pairs in the poloniex bundle, the default denomination
# currently supported by Catalyst is 1/1000th of a full coin. Use this
# constant to scale the price of up to that of a full coin if desired.
context.TICK_SIZE = 1000.0
context.is_buying = True context.is_buying = True
context.asset = symbol(context.ASSET_NAME) context.asset = symbol(context.ASSET_NAME)
context.i = 0 context.i = 0
def handle_data(context, data): def handle_data(context, data):
context.i += 1 context.i += 1
@@ -64,8 +62,8 @@ def handle_data(context, data):
order_target_value( order_target_value(
context.asset, context.asset,
target_hodl_value, target_hodl_value,
limit_price=price*1.1, limit_price=price * 1.1,
stop_price=price*0.9, stop_price=price * 0.9,
) )
record( record(
@@ -76,6 +74,7 @@ def handle_data(context, data):
leverage=context.account.leverage, leverage=context.account.leverage,
) )
def analyze(context=None, results=None): def analyze(context=None, results=None):
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@@ -86,7 +85,7 @@ def analyze(context=None, results=None):
ax2 = plt.subplot(612, sharex=ax1) ax2 = plt.subplot(612, sharex=ax1)
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME)) ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
(context.TICK_SIZE * results[['price']]).plot(ax=ax2) results[['price']].plot(ax=ax2)
trans = results.ix[[t != [] for t in results.transactions]] trans = results.ix[[t != [] for t in results.transactions]]
buys = trans.ix[ buys = trans.ix[
@@ -94,7 +93,7 @@ def analyze(context=None, results=None):
] ]
ax2.plot( ax2.plot(
buys.index, buys.index,
context.TICK_SIZE * results.price[buys.index], results.price[buys.index],
'^', '^',
markersize=10, markersize=10,
color='g', color='g',
@@ -135,3 +134,4 @@ def analyze(context=None, results=None):
# Show the plot. # Show the plot.
plt.gcf().set_size_inches(18, 8) plt.gcf().set_size_inches(18, 8)
plt.show() plt.show()
+275
View File
@@ -0,0 +1,275 @@
# For this example, we're going to write a simple momentum script. When the
# stock goes up quickly, we're going to buy; when it goes down quickly, we're
# going to sell. Hopefully we'll ride the waves.
import os
import tempfile
import time
import pandas as pd
import talib
from logbook import Logger
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
NAMESPACE = 'mean_reversion_simple'
log = Logger(NAMESPACE)
# 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_eth = symbol('neo_eth')
context.base_price = None
context.current_day = None
context.RSI_OVERSOLD = 50
context.RSI_OVERBOUGHT = 80
context.CANDLE_SIZE = '5T'
context.start_time = time.time()
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.
# 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
# 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.
# 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
)
# 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_eth, 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_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
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 Value ({})'.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(
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 ({})'.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')
ax6 = plt.subplot(615, sharex=ax1)
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
ax6.axhline(70, color='darkgoldenrod')
ax6.axhline(30, 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)
# 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 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-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='eth',
live_graph=False
)
+276
View File
@@ -0,0 +1,276 @@
from datetime import timedelta
import pandas as pd
import numpy as np
import talib
from logbook import Logger
from catalyst.api import (
order,
symbol,
record,
get_open_orders,
)
from catalyst.exchange.stats_utils import crossover, crossunder
from catalyst.utils.run_algo import run_algorithm
algo_namespace = 'rsi'
log = Logger(algo_namespace)
def initialize(context):
log.info('initializing algo')
context.asset = symbol('eth_btc')
context.base_price = None
context.MAX_HOLDINGS = 0.2
context.RSI_OVERSOLD = 30
context.RSI_OVERSOLD_BBANDS = 45
context.RSI_OVERBOUGHT_BBANDS = 55
context.SLIPPAGE_ALLOWED = 0.03
context.TARGET = 0.15
context.STOP_LOSS = 0.1
context.STOP = 0.03
context.position = None
context.last_bar = None
context.errors = []
pass
def _handle_buy_sell_decision(context, data, signal, price):
orders = get_open_orders(context.asset)
if len(orders) > 0:
log.info('skipping bar until all open orders execute')
return
positions = context.portfolio.positions
if context.position is None and context.asset in positions:
position = positions[context.asset]
context.position = dict(
cost_basis=position['cost_basis'],
amount=position['amount'],
stop=None
)
action = None
if context.position is not None:
cost_basis = context.position['cost_basis']
amount = context.position['amount']
log.info(
'found {amount} positions with cost basis {cost_basis}'.format(
amount=amount,
cost_basis=cost_basis
)
)
stop = context.position['stop']
target = cost_basis * (1 + context.TARGET)
if price >= target:
context.position['cost_basis'] = price
context.position['stop'] = context.STOP
stop_target = context.STOP_LOSS if stop is None else context.STOP
if price < cost_basis * (1 - stop_target):
log.info('executing stop loss')
order(
asset=context.asset,
amount=-amount,
limit_price=price * (1 - context.SLIPPAGE_ALLOWED),
)
action = 0
context.position = None
else:
if signal == 'long':
log.info('opening position')
buy_amount = context.MAX_HOLDINGS / price
order(
asset=context.asset,
amount=buy_amount,
limit_price=price * (1 + context.SLIPPAGE_ALLOWED),
)
context.position = dict(
cost_basis=price,
amount=buy_amount,
stop=None
)
action = 0
def _handle_data_rsi_only(context, data):
price = data.current(context.asset, 'close')
log.info('got price {price}'.format(price=price))
if price is np.nan:
log.warn('no pricing data')
return
if context.base_price is None:
context.base_price = price
try:
prices = data.history(
context.asset,
fields='price',
bar_count=17,
frequency='30T'
)
except Exception as e:
log.warn('historical data not available: '.format(e))
return
rsi = talib.RSI(prices.values, timeperiod=16)[-1]
log.info('got rsi {}'.format(rsi))
signal = None
if rsi < context.RSI_OVERSOLD:
signal = 'long'
# Making sure that the price is still current
price = data.current(context.asset, 'close')
cash = context.portfolio.cash
log.info(
'base currency available: {cash}, cap: {cap}'.format(
cash=cash,
cap=context.MAX_HOLDINGS
)
)
volume = data.current(context.asset, 'volume')
price_change = (price - context.base_price) / context.base_price
record(
price=price,
price_change=price_change,
rsi=rsi,
volume=volume,
cash=cash,
starting_cash=context.portfolio.starting_cash,
leverage=context.account.leverage,
)
_handle_buy_sell_decision(context, data, signal, price)
def handle_data(context, data):
dt = data.current_dt
if context.last_bar is None or (
context.last_bar + timedelta(minutes=15)) <= dt:
context.last_bar = dt
else:
return
log.info('BAR {}'.format(dt))
try:
_handle_data_rsi_only(context, data)
except Exception as e:
log.warn('aborting the bar on error {}'.format(e))
context.errors.append(e)
if len(context.errors) > 0:
log.info('the errors:\n{}'.format(context.errors))
def analyze(context=None, results=None):
import matplotlib.pyplot as plt
base_currency = context.exchanges.values()[0].base_currency.upper()
# Plot the portfolio and asset data.
ax1 = plt.subplot(611)
results.loc[:, 'portfolio_value'].plot(ax=ax1)
ax1.set_ylabel('Portfolio Value ({})'.format(base_currency))
ax2 = plt.subplot(612, sharex=ax1)
results.loc[:, 'price'].plot(ax=ax2)
ax2.set_ylabel('{asset} ({base})'.format(
asset=context.asset.symbol, base=base_currency
))
trans = results.loc[[t != [] for t in results.transactions], :]
buys = trans.loc[[t[0]['amount'] > 0 for t in trans.transactions], :]
sells = trans.loc[[t[0]['amount'] < 0 for t in trans.transactions], :]
# buys = results.loc[results['action'] == 1, :]
# sells = results.loc[results['action'] == 0, :]
ax2.plot(
buys.index,
results.loc[buys.index, 'price'],
'^',
markersize=10,
color='g',
)
ax2.plot(
sells.index,
results.loc[sells.index, 'price'],
'v',
markersize=10,
color='r',
)
ax3 = plt.subplot(613, sharex=ax1)
results.loc[:, ['alpha', 'beta']].plot(ax=ax3)
ax3.set_ylabel('Alpha / Beta ')
ax4 = plt.subplot(614, sharex=ax1)
results.loc[:, ['starting_cash', 'cash']].plot(ax=ax4)
ax4.set_ylabel('Base Currency ({})'.format(base_currency))
results['algorithm'] = results.loc[:, 'algorithm_period_return']
ax5 = plt.subplot(615, sharex=ax1)
results.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
ax5.set_ylabel('Percent Change')
ax6 = plt.subplot(616, sharex=ax1)
results.loc[:, 'rsi'].plot(ax=ax6)
ax6.set_ylabel('RSI')
ax6.plot(
buys.index,
results.loc[buys.index, 'rsi'],
'^',
markersize=10,
color='g',
)
ax6.plot(
sells.index,
results.loc[sells.index, 'rsi'],
'v',
markersize=10,
color='r',
)
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
pass
# run_algorithm(
# initialize=initialize,
# handle_data=handle_data,
# analyze=analyze,
# exchange_name='bittrex',
# live=True,
# algo_namespace=algo_namespace,
# base_currency='btc',
# live_graph=False
# )
# Backtest
run_algorithm(
capital_base=0.5,
data_frequency='minute',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='poloniex',
algo_namespace=algo_namespace,
base_currency='btc',
start=pd.to_datetime('2017-9-1', utc=True),
end=pd.to_datetime('2017-10-1', utc=True),
)
+89 -8
View File
@@ -2,12 +2,15 @@ import talib
import pandas as pd import pandas as pd
from catalyst import run_algorithm from catalyst import run_algorithm
from catalyst.api import symbol from catalyst.api import symbol, record
from catalyst.exchange.stats_utils import get_pretty_stats, \
extract_transactions
def initialize(context): def initialize(context):
print('initializing') print('initializing')
context.asset = symbol('eth_btc') context.asset = symbol('neo_usd')
context.base_price = None
def handle_data(context, data): def handle_data(context, data):
@@ -20,26 +23,104 @@ def handle_data(context, data):
prices = data.history( prices = data.history(
context.asset, context.asset,
fields='price', fields='price',
bar_count=16, bar_count=14,
frequency='5T' frequency='15T'
) )
rsi = talib.RSI(prices.values, timeperiod=14)[-1] rsi = talib.RSI(prices.values, timeperiod=14)[-1]
print('got rsi: {}'.format(rsi)) print('got rsi: {}'.format(rsi))
except Exception as e: except Exception as e:
print(e) print(e)
# 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,
price_change=price_change,
cash=cash
)
def analyze(context, perf):
import matplotlib.pyplot as plt
print('the stats: {}'.format(get_pretty_stats(perf)))
# 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 Value ({})'.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(
asset=context.asset.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, '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=''
)
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))
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')
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
pass
run_algorithm( run_algorithm(
capital_base=250, capital_base=250,
start=pd.to_datetime('2016-6-1', utc=True), start=pd.to_datetime('2017-11-1 0:00', utc=True),
end=pd.to_datetime('2016-12-31', utc=True), end=pd.to_datetime('2017-11-10 23:59', utc=True),
data_frequency='daily', data_frequency='daily',
initialize=initialize, initialize=initialize,
handle_data=handle_data, handle_data=handle_data,
analyze=None, analyze=analyze,
exchange_name='bitfinex', exchange_name='bitfinex',
algo_namespace='simple_loop', algo_namespace='simple_loop',
base_currency='btc' base_currency='usd'
) )
# run_algorithm( # run_algorithm(
# initialize=initialize, # initialize=initialize,
+129
View File
@@ -0,0 +1,129 @@
"""
Requires Catalyst version 0.3.0 or above
Tested on Catalyst version 0.3.3
These example aims to provide and easy way for users to learn how to collect data from the different exchanges.
You simply need to specify the exchange and the market that you want to focus on.
You will all see how to create a universe and filter it base on the exchange and the market you desire.
The example prints out the closing price of all the pairs for a given market-exchange every 30 minutes.
The example also contains the ohlcv minute data for the past seven days which could be used to create indicators
Use this as the backbone to create your own trading strategies.
Variables lookback date and date are used to ensure data for a coin existed on the lookback period specified.
"""
import numpy as np
import pandas as pd
from datetime import timedelta
from catalyst import run_algorithm
from catalyst.exchange.exchange_utils import get_exchange_symbols
from catalyst.api import (
symbols,
)
def initialize(context):
context.i = -1 # counts the minutes
context.exchange = context.exchanges.values()[0].name.lower() # exchange name
context.base_currency = context.exchanges.values()[0].base_currency.lower() # market base currency
def handle_data(context, data):
context.i += 1
lookback_days = 7 # 7 days
# current date formatted into a string
today = data.current_dt
date, time = today.strftime('%Y-%m-%d %H:%M:%S').split(' ')
lookback_date = today - timedelta(days=lookback_days) # subtract the amount of days specified in lookback
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[0] # get only the date as a string
# update universe everyday
new_day = 60 * 24 # assuming data_frequency='minute'
if not context.i % new_day:
context.universe = universe(context, lookback_date, date)
# get data every 30 minutes
minutes = 30
one_day_in_minutes = 1440 # 1440 assumes data_frequency='minute'
lookback = one_day_in_minutes / minutes * lookback_days # get N lookback_days of history data
if not context.i % minutes and context.universe:
# we iterate for every pair in the current universe
for coin in context.coins:
pair = str(coin.symbol)
# 30 minute interval ohlcv data (the standard data required for candlestick or indicators/signals)
# 30T means 30 minutes re-sampling of one minute data. change to your desire time interval.
opened = fill(data.history(coin, 'open', bar_count=lookback, frequency='30T')).values
high = fill(data.history(coin, 'high', bar_count=lookback, frequency='30T')).values
low = fill(data.history(coin, 'low', bar_count=lookback, frequency='30T')).values
close = fill(data.history(coin, 'price', bar_count=lookback, frequency='30T')).values
volume = fill(data.history(coin, 'volume', bar_count=lookback, frequency='30T')).values
# close[-1] is the equivalent to current price
# displays the minute price for each pair every 30 minutes
print(today, pair, opened[-1], high[-1], low[-1], close[-1], volume[-1])
# ----------------------------------------------------------------------------------------------------------
# -------------------------------------- Insert Your Strategy Here -----------------------------------------
# ----------------------------------------------------------------------------------------------------------
def analyze(context=None, results=None):
pass
# Get the universe for a given exchange and a given base_currency market
# Example: Poloniex BTC Market
def universe(context, lookback_date, current_date):
json_symbols = get_exchange_symbols(context.exchange) # get all the pairs for the exchange
universe_df = pd.DataFrame.from_dict(json_symbols).transpose().astype(str) # convert into a dataframe
universe_df['base_currency'] = universe_df.apply(lambda row: row.symbol.split('_')[1],
axis=1)
universe_df['market_currency'] = universe_df.apply(lambda row: row.symbol.split('_')[0],
axis=1)
# Filter all the exchange pairs to only the ones for a give base currency
universe_df = universe_df[universe_df['base_currency'] == context.base_currency]
# Filter all the pairs to ensure that pair existed in the current date range
universe_df = universe_df[universe_df.start_date < lookback_date]
universe_df = universe_df[universe_df.end_daily >= current_date]
context.coins = symbols(*universe_df.symbol) # convert all the pairs to symbols
# print(universe_df.symbol.tolist())
return universe_df.symbol.tolist()
# Replace all NA, NAN or infinite values with its nearest value
def fill(series):
if isinstance(series, pd.Series):
return series.replace([np.inf, -np.inf], np.nan).ffill().bfill()
elif isinstance(series, np.ndarray):
return pd.Series(series).replace([np.inf, -np.inf], np.nan).ffill().bfill().values
else:
return series
if __name__ == '__main__':
start_date = pd.to_datetime('2017-11-10', utc=True)
end_date = pd.to_datetime('2017-11-13', utc=True)
performance = run_algorithm(start=start_date, end=end_date,
capital_base=100.0, # amount of base_currency, not always in dollars unless usd
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
data_frequency='minute',
base_currency='btc',
live=False,
live_graph=False,
algo_namespace='simple_universe')
"""
Run in Terminal (inside catalyst environment):
python simple_universe.py
"""
+364
View File
@@ -0,0 +1,364 @@
# Run Command
# catalyst run --start 2017-1-1 --end 2017-11-1 -o talib_simple.pickle -f talib_simple.py -x poloniex
#
# Description
# Simple TALib Example showing how to use various indicators in you strategy
# Based loosly on https://github.com/mellertson/talib-macd-example/blob/master/talib-macd-matplotlib-example.py
import os
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import talib as ta
from logbook import Logger
from matplotlib.dates import date2num
from matplotlib.finance import candlestick_ohlc
from catalyst import run_algorithm
from catalyst.api import (
order,
order_target_percent,
symbol,
)
from catalyst.exchange.stats_utils import get_pretty_stats
algo_namespace = 'talib_sample'
log = Logger(algo_namespace)
def initialize(context):
log.info('Starting TALib Simple Example')
context.ASSET_NAME = 'BTC_USDT'
context.asset = symbol(context.ASSET_NAME)
context.ORDER_SIZE = 10
context.SLIPPAGE_ALLOWED = 0.05
context.swallow_errors = True
context.errors = []
# Bars to look at per iteration should be bigger than SMA_SLOW
context.BARS = 365
context.COUNT = 0
# Technical Analysis Settings
context.SMA_FAST = 50
context.SMA_SLOW = 100
context.RSI_PERIOD = 14
context.RSI_OVER_BOUGHT = 80
context.RSI_OVER_SOLD = 20
context.RSI_AVG_PERIOD = 15
context.MACD_FAST = 12
context.MACD_SLOW = 26
context.MACD_SIGNAL = 9
context.STOCH_K = 14
context.STOCH_D = 3
context.STOCH_OVER_BOUGHT = 80
context.STOCH_OVER_SOLD = 20
pass
def _handle_data(context, data):
# Get price, open, high, low, close
prices = data.history(
context.asset,
bar_count=context.BARS,
fields=['price', 'open', 'high', 'low', 'close'],
frequency='1d')
# Create a analysis data frame
analysis = pd.DataFrame(index=prices.index)
# SMA FAST
analysis['sma_f'] = ta.SMA(prices.close.as_matrix(), context.SMA_FAST)
# SMA SLOW
analysis['sma_s'] = ta.SMA(prices.close.as_matrix(), context.SMA_SLOW)
# Relative Strength Index
analysis['rsi'] = ta.RSI(prices.close.as_matrix(), context.RSI_PERIOD)
# RSI SMA
analysis['sma_r'] = ta.SMA(analysis.rsi.as_matrix(),
context.RSI_AVG_PERIOD)
# MACD, MACD Signal, MACD Histogram
analysis['macd'], analysis['macdSignal'], analysis['macdHist'] = ta.MACD(
prices.close.as_matrix(), fastperiod=context.MACD_FAST,
slowperiod=context.MACD_SLOW, signalperiod=context.MACD_SIGNAL)
# Stochastics %K %D
# %K = (Current Close - Lowest Low)/(Highest High - Lowest Low) * 100
# %D = 3-day SMA of %K
analysis['stoch_k'], analysis['stoch_d'] = ta.STOCH(
prices.high.as_matrix(), prices.low.as_matrix(),
prices.close.as_matrix(), slowk_period=context.STOCH_K,
slowd_period=context.STOCH_D)
# SMA FAST over SLOW Crossover
analysis['sma_test'] = np.where(analysis.sma_f > analysis.sma_s, 1, 0)
# MACD over Signal Crossover
analysis['macd_test'] = np.where((analysis.macd > analysis.macdSignal), 1,
0)
# Stochastics OVER BOUGHT & Decreasing
analysis['stoch_over_bought'] = np.where(
(analysis.stoch_k > context.STOCH_OVER_BOUGHT) & (
analysis.stoch_k > analysis.stoch_k.shift(1)), 1, 0)
# Stochastics OVER SOLD & Increasing
analysis['stoch_over_sold'] = np.where(
(analysis.stoch_k < context.STOCH_OVER_SOLD) & (
analysis.stoch_k > analysis.stoch_k.shift(1)), 1, 0)
# RSI OVER BOUGHT & Decreasing
analysis['rsi_over_bought'] = np.where(
(analysis.rsi > context.RSI_OVER_BOUGHT) & (
analysis.rsi < analysis.rsi.shift(1)), 1, 0)
# RSI OVER SOLD & Increasing
analysis['rsi_over_sold'] = np.where(
(analysis.rsi < context.RSI_OVER_SOLD) & (
analysis.rsi > analysis.rsi.shift(1)), 1, 0)
# Save the prices and analysis to send to analyze
context.prices = prices
context.analysis = analysis
context.price = data.current(context.asset, 'price')
makeOrders(context, analysis)
# Log the values of this bar
logAnalysis(analysis)
def handle_data(context, data):
log.info('handling bar {}'.format(data.current_dt))
try:
_handle_data(context, data)
except Exception as e:
log.warn('aborting the bar on error {}'.format(e))
context.errors.append(e)
log.info('completed bar {}, total execution errors {}'.format(
data.current_dt,
len(context.errors)
))
if len(context.errors) > 0:
log.info('the errors:\n{}'.format(context.errors))
def analyze(context, results):
# Save results in CSV file
filename = os.path.splitext(os.path.basename('talib_simple'))[0]
results.to_csv(filename + '.csv')
log.info('the daily stats:\n{}'.format(get_pretty_stats(results)))
chart(context, context.prices, context.analysis, results)
pass
def makeOrders(context, analysis):
if context.asset in context.portfolio.positions:
# Current position
position = context.portfolio.positions[context.asset]
if (position == 0):
log.info('Position Zero')
return
# Cost Basis
cost_basis = position.cost_basis
log.info(
'Holdings: {amount} @ {cost_basis}'.format(
amount=position.amount,
cost_basis=cost_basis
)
)
# Sell when holding and got sell singnal
if isSell(context, analysis):
profit = (context.price * position.amount) - (
cost_basis * position.amount)
order_target_percent(
asset=context.asset,
target=0,
limit_price=context.price * (1 - context.SLIPPAGE_ALLOWED),
)
log.info(
'Sold {amount} @ {price} Profit: {profit}'.format(
amount=position.amount,
price=context.price,
profit=profit
)
)
else:
log.info('no buy or sell opportunity found')
else:
# Buy when not holding and got buy signal
if isBuy(context, analysis):
order(
asset=context.asset,
amount=context.ORDER_SIZE,
limit_price=context.price * (1 + context.SLIPPAGE_ALLOWED)
)
log.info(
'Bought {amount} @ {price}'.format(
amount=context.ORDER_SIZE,
price=context.price
)
)
def isBuy(context, analysis):
# Bullish SMA Crossover
if (getLast(analysis, 'sma_test') == 1):
# Bullish MACD
if (getLast(analysis, 'macd_test') == 1):
return True
# # Bullish Stochastics
# if(getLast(analysis, 'stoch_over_sold') == 1):
# return True
# # Bullish RSI
# if(getLast(analysis, 'rsi_over_sold') == 1):
# return True
return False
def isSell(context, analysis):
# Bearish SMA Crossover
if (getLast(analysis, 'sma_test') == 0):
# Bearish MACD
if (getLast(analysis, 'macd_test') == 0):
return True
# # Bearish Stochastics
# if(getLast(analysis, 'stoch_over_bought') == 0):
# return True
# # Bearish RSI
# if(getLast(analysis, 'rsi_over_bought') == 0):
# return True
return False
def chart(context, prices, analysis, results):
results.portfolio_value.plot()
# Data for matplotlib finance plot
dates = date2num(prices.index.to_pydatetime())
# Create the Open High Low Close Tuple
prices_ohlc = [tuple([dates[i],
prices.open[i],
prices.high[i],
prices.low[i],
prices.close[i]]) for i in range(len(dates))]
fig = plt.figure(figsize=(14, 18))
# Draw the candle sticks
ax1 = fig.add_subplot(411)
ax1.set_ylabel(context.ASSET_NAME, size=20)
candlestick_ohlc(ax1, prices_ohlc, width=0.4, colorup='g', colordown='r')
# Draw Moving Averages
analysis.sma_f.plot(ax=ax1, c='r')
analysis.sma_s.plot(ax=ax1, c='g')
# RSI
ax2 = fig.add_subplot(412)
ax2.set_ylabel('RSI', size=12)
analysis.rsi.plot(ax=ax2, c='g',
label='Period: ' + str(context.RSI_PERIOD))
analysis.sma_r.plot(ax=ax2, c='r',
label='MA: ' + str(context.RSI_AVG_PERIOD))
ax2.axhline(y=30, c='b')
ax2.axhline(y=50, c='black')
ax2.axhline(y=70, c='b')
ax2.set_ylim([0, 100])
handles, labels = ax2.get_legend_handles_labels()
ax2.legend(handles, labels)
# Draw MACD computed with Talib
ax3 = fig.add_subplot(413)
ax3.set_ylabel('MACD: ' + str(context.MACD_FAST) + ', ' + str(
context.MACD_SLOW) + ', ' + str(context.MACD_SIGNAL), size=12)
analysis.macd.plot(ax=ax3, color='b', label='Macd')
analysis.macdSignal.plot(ax=ax3, color='g', label='Signal')
analysis.macdHist.plot(ax=ax3, color='r', label='Hist')
ax3.axhline(0, lw=2, color='0')
handles, labels = ax3.get_legend_handles_labels()
ax3.legend(handles, labels)
# Stochastic plot
ax4 = fig.add_subplot(414)
ax4.set_ylabel('Stoch (k,d)', size=12)
analysis.stoch_k.plot(ax=ax4, label='stoch_k:' + str(context.STOCH_K),
color='r')
analysis.stoch_d.plot(ax=ax4, label='stoch_d:' + str(context.STOCH_D),
color='g')
handles, labels = ax4.get_legend_handles_labels()
ax4.legend(handles, labels)
ax4.axhline(y=20, c='b')
ax4.axhline(y=50, c='black')
ax4.axhline(y=80, c='b')
plt.show()
def logAnalysis(analysis):
# Log only the last value in the array
log.info('- sma_f: {:.2f}'.format(getLast(analysis, 'sma_f')))
log.info('- sma_s: {:.2f}'.format(getLast(analysis, 'sma_s')))
log.info('- rsi: {:.2f}'.format(getLast(analysis, 'rsi')))
log.info('- sma_r: {:.2f}'.format(getLast(analysis, 'sma_r')))
log.info('- macd: {:.2f}'.format(getLast(analysis, 'macd')))
log.info(
'- macdSignal: {:.2f}'.format(getLast(analysis, 'macdSignal')))
log.info('- macdHist: {:.2f}'.format(getLast(analysis, 'macdHist')))
log.info('- stoch_k: {:.2f}'.format(getLast(analysis, 'stoch_k')))
log.info('- stoch_d: {:.2f}'.format(getLast(analysis, 'stoch_d')))
log.info('- sma_test: {}'.format(getLast(analysis, 'sma_test')))
log.info('- macd_test: {}'.format(getLast(analysis, 'macd_test')))
log.info('- stoch_over_bought: {}'.format(
getLast(analysis, 'stoch_over_bought')))
log.info(
'- stoch_over_sold: {}'.format(getLast(analysis, 'stoch_over_sold')))
log.info('- rsi_over_bought: {}'.format(
getLast(analysis, 'rsi_over_bought')))
log.info(
'- rsi_over_sold: {}'.format(getLast(analysis, 'rsi_over_sold')))
def getLast(arr, name):
return arr[name][arr[name].index[-1]]
if __name__ == '__main__':
run_algorithm(
capital_base=10000,
data_frequency='daily',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='poloniex',
base_currency='usdt',
start=pd.to_datetime('2016-11-1', utc=True),
end=pd.to_datetime('2017-11-10', utc=True),
)
+14 -8
View File
@@ -41,14 +41,15 @@ class AssetFinderExchange(object):
SidsNotFound SidsNotFound
When a requested sid is not found and default_none=False. When a requested sid is not found and default_none=False.
""" """
for sid in sids: # for sid in sids:
if sid in self._asset_cache: # if sid in self._asset_cache:
log.debug('got asset from cache: {}'.format(sid)) # log.debug('got asset from cache: {}'.format(sid))
else: # else:
log.debug('fetching asset: {}'.format(sid)) # log.debug('fetching asset: {}'.format(sid))
return list() return list()
def lookup_symbol(self, symbol, exchange, as_of_date=None, fuzzy=False): def lookup_symbol(self, symbol, exchange, data_frequency=None,
as_of_date=None, fuzzy=False):
"""Lookup an asset by symbol. """Lookup an asset by symbol.
Parameters Parameters
@@ -84,10 +85,15 @@ class AssetFinderExchange(object):
""" """
log.debug('looking up symbol: {} {}'.format(symbol, exchange.name)) log.debug('looking up symbol: {} {}'.format(symbol, exchange.name))
key = ','.join([exchange.name, symbol]) if data_frequency is not None:
key = ','.join([exchange.name, symbol, data_frequency])
else:
key = ','.join([exchange.name, symbol])
if key in self._asset_cache: if key in self._asset_cache:
return self._asset_cache[key] return self._asset_cache[key]
else: else:
asset = exchange.get_asset(symbol) asset = exchange.get_asset(symbol, data_frequency)
self._asset_cache[key] = asset self._asset_cache[key] = asset
return asset return asset
+7 -2
View File
@@ -46,8 +46,13 @@ class Bitfinex(Exchange):
self.secret = secret.encode('UTF-8') self.secret = secret.encode('UTF-8')
self.name = 'bitfinex' self.name = 'bitfinex'
self.color = 'green' self.color = 'green'
self.assets = {}
self.assets = dict()
self.load_assets() self.load_assets()
self.local_assets = dict()
self.load_assets(is_local=True)
self.base_currency = base_currency self.base_currency = base_currency
self._portfolio = portfolio self._portfolio = portfolio
self.minute_writer = None self.minute_writer = None
@@ -61,7 +66,7 @@ class Bitfinex(Exchange):
self.max_requests_per_minute = 80 self.max_requests_per_minute = 80
self.request_cpt = dict() self.request_cpt = dict()
self.bundle = ExchangeBundle(self) self.bundle = ExchangeBundle(self.name)
def _request(self, operation, data, version='v1'): def _request(self, operation, data, version='v1'):
payload_object = { payload_object = {
+4 -1
View File
@@ -46,7 +46,10 @@ class Bittrex(Exchange):
self.assets = dict() self.assets = dict()
self.load_assets() self.load_assets()
self.bundle = ExchangeBundle(self) self.local_assets = dict()
self.load_assets(is_local=True)
self.bundle = ExchangeBundle(self.name)
@property @property
def account(self): def account(self):
+4 -2
View File
@@ -3,11 +3,12 @@ import json
import time import time
import hmac import hmac
import hashlib import hashlib
import ssl
# Workaround for backwards compatibility # Workaround for backwards compatibility
# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7 # https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7
from six.moves import urllib from six.moves import urllib
urlopen = urllib.request.urlopen urlopen = urllib.request.urlopen
@@ -48,7 +49,8 @@ class Bittrex_api(object):
headers = {} headers = {}
req = urllib.request.Request(url, headers=headers) req = urllib.request.Request(url, headers=headers)
response = json.loads(urlopen(req).read()) response = json.loads(urlopen(
req, context=ssl._create_unverified_context()).read())
if response["result"]: if response["result"]:
return response["result"] return response["result"]
+45 -2
View File
@@ -6,9 +6,11 @@ from datetime import timedelta, datetime, date
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pytz import pytz
from catalyst.assets._assets import TradingPair
from catalyst.data.bundles.core import download_without_progress from catalyst.data.bundles.core import download_without_progress
from catalyst.exchange.exchange_utils import get_exchange_bundles_folder from catalyst.exchange.exchange_utils import get_exchange_bundles_folder, \
get_exchange_symbols
EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex'] EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex']
API_URL = 'http://data.enigma.co/api/v1' API_URL = 'http://data.enigma.co/api/v1'
@@ -149,7 +151,7 @@ def get_periods(start_dt, end_dt, freq):
return len(get_periods_range(start_dt, end_dt, freq)) return len(get_periods_range(start_dt, end_dt, freq))
def get_start_dt(end_dt, bar_count, data_frequency): def get_start_dt(end_dt, bar_count, data_frequency, include_first=True):
""" """
The start date based on specified end date and data frequency. The start date based on specified end date and data frequency.
@@ -168,6 +170,9 @@ def get_start_dt(end_dt, bar_count, data_frequency):
if periods > 1: if periods > 1:
delta = get_delta(periods, data_frequency) delta = get_delta(periods, data_frequency)
start_dt = end_dt - delta start_dt = end_dt - delta
if not include_first:
start_dt += get_delta(1, data_frequency)
else: else:
start_dt = end_dt start_dt = end_dt
@@ -314,3 +319,41 @@ def range_in_bundle(asset, start_dt, end_dt, reader):
has_data = False has_data = False
return has_data return has_data
def get_assets(exchange, include_symbols, exclude_symbols):
"""
Get assets from an exchange, including or excluding the specified
symbols.
Parameters
----------
exchange: Exchange
include_symbols: str
exclude_symbols: str
Returns
-------
list[TradingPair]
"""
if include_symbols is not None:
include_symbols_list = include_symbols.split(',')
return exchange.get_assets(include_symbols_list)
else:
all_assets = exchange.get_assets()
if exclude_symbols is not None:
exclude_symbols_list = exclude_symbols.split(',')
assets = []
for asset in all_assets:
if asset.symbol not in exclude_symbols_list:
assets.append(asset)
return assets
else:
return all_assets
+79 -45
View File
@@ -16,7 +16,7 @@ from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \ from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange, \ InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange, \
PricingDataNotLoadedError, \ PricingDataNotLoadedError, \
NoDataAvailableOnExchange NoDataAvailableOnExchange, ExchangeSymbolsNotFound
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \ from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
ExchangeLimitOrder, ExchangeStopOrder ExchangeLimitOrder, ExchangeStopOrder
from catalyst.exchange.exchange_portfolio import ExchangePortfolio from catalyst.exchange.exchange_portfolio import ExchangePortfolio
@@ -24,7 +24,6 @@ from catalyst.exchange.exchange_utils import get_exchange_symbols, \
get_frequency, resample_history_df get_frequency, resample_history_df
from catalyst.finance.order import ORDER_STATUS from catalyst.finance.order import ORDER_STATUS
from catalyst.finance.transaction import Transaction from catalyst.finance.transaction import Transaction
from catalyst.utils.deprecate import deprecated
log = Logger('Exchange', level=LOG_LEVEL) log = Logger('Exchange', level=LOG_LEVEL)
@@ -34,7 +33,8 @@ class Exchange:
def __init__(self): def __init__(self):
self.name = None self.name = None
self.assets = {} self.assets = dict()
self.local_assets = dict()
self._portfolio = None self._portfolio = None
self.minute_writer = None self.minute_writer = None
self.minute_reader = None self.minute_reader = None
@@ -43,7 +43,7 @@ class Exchange:
self.num_candles_limit = None self.num_candles_limit = None
self.max_requests_per_minute = None self.max_requests_per_minute = None
self.request_cpt = None self.request_cpt = None
self.bundle = ExchangeBundle(self) self.bundle = ExchangeBundle(self.name)
@property @property
def positions(self): def positions(self):
@@ -174,7 +174,7 @@ class Exchange:
return symbols return symbols
def get_assets(self, symbols=None): def get_assets(self, symbols=None, data_frequency=None):
""" """
The list of markets for the specified symbols. The list of markets for the specified symbols.
@@ -191,7 +191,7 @@ class Exchange:
if symbols is not None: if symbols is not None:
for symbol in symbols: for symbol in symbols:
asset = self.get_asset(symbol) asset = self.get_asset(symbol, data_frequency)
assets.append(asset) assets.append(asset)
else: else:
for key in self.assets: for key in self.assets:
@@ -199,7 +199,19 @@ class Exchange:
return assets return assets
def get_asset(self, symbol): def _find_asset(self, asset, symbol, data_frequency, is_local=False):
assets = self.assets if not is_local else self.local_assets
for key in assets:
if not asset and assets[key].symbol.lower() == symbol.lower() and (
not data_frequency or (
data_frequency == 'minute' and assets[
key].end_minute is not None)):
asset = assets[key]
return asset
def get_asset(self, symbol, data_frequency=None):
""" """
The market for the specified symbol. The market for the specified symbol.
@@ -214,13 +226,17 @@ class Exchange:
""" """
asset = None asset = None
for key in self.assets: log.debug('searching asset {} on the server')
if not asset and self.assets[key].symbol.lower() == symbol.lower(): asset = self._find_asset(asset, symbol, data_frequency, False)
asset = self.assets[key]
log.debug('asset {} not found on the server, searching local assets')
asset = self._find_asset(asset, symbol, data_frequency, True)
if not asset: if not asset:
all_values = list(self.assets.values()) + \
list(self.local_assets.values())
supported_symbols = [ supported_symbols = [
pair.symbol for pair in list(self.assets.values()) asset.symbol for asset in all_values
] ]
raise SymbolNotFoundOnExchange( raise SymbolNotFoundOnExchange(
@@ -231,10 +247,10 @@ class Exchange:
return asset return asset
def fetch_symbol_map(self): def fetch_symbol_map(self, is_local=False):
return get_exchange_symbols(self.name) return get_exchange_symbols(self.name, is_local)
def load_assets(self): def load_assets(self, is_local=False):
""" """
Populate the 'assets' attribute with a dictionary of Assets. Populate the 'assets' attribute with a dictionary of Assets.
The key of the resulting dictionary is the exchange specific The key of the resulting dictionary is the exchange specific
@@ -247,11 +263,15 @@ class Exchange:
universal symbol. This simple approach avoids maintaining a mapping universal symbol. This simple approach avoids maintaining a mapping
of sids. of sids.
This method can be overridden if an exchange offers equivalent data This method can be omerridden if an exchange offers equivalent data
via its api. via its api.
""" """
symbol_map = self.fetch_symbol_map() try:
symbol_map = self.fetch_symbol_map(is_local)
except ExchangeSymbolsNotFound:
return None
for exchange_symbol in symbol_map: for exchange_symbol in symbol_map:
asset = symbol_map[exchange_symbol] asset = symbol_map[exchange_symbol]
@@ -303,7 +323,10 @@ class Exchange:
exchange_symbol=exchange_symbol exchange_symbol=exchange_symbol
) )
self.assets[exchange_symbol] = trading_pair if is_local:
self.local_assets[exchange_symbol] = trading_pair
else:
self.assets[exchange_symbol] = trading_pair
def check_open_orders(self): def check_open_orders(self):
""" """
@@ -468,15 +491,14 @@ class Exchange:
return series return series
@deprecated def get_history_window(self,
def get_history_window_direct(self, assets,
assets, end_dt,
end_dt, bar_count,
bar_count, frequency,
frequency, field,
field, data_frequency=None,
data_frequency=None, ffill=True):
ffill=True):
""" """
Public API method that returns a dataframe containing the requested Public API method that returns a dataframe containing the requested
@@ -514,35 +536,46 @@ class Exchange:
A dataframe containing the requested data. A dataframe containing the requested data.
""" """
start_dt = get_start_dt(end_dt, bar_count, data_frequency) freq, candle_size, unit, data_frequency = get_frequency(
frequency, data_frequency
)
adj_bar_count = candle_size * bar_count
start_dt = get_start_dt(end_dt, adj_bar_count, data_frequency)
# The get_history method supports multiple asset # The get_history method supports multiple asset
candles = self.get_candles( candles = self.get_candles(
data_frequency=frequency, freq=freq,
assets=assets, assets=assets,
bar_count=bar_count, bar_count=bar_count,
start_dt=start_dt, start_dt=start_dt,
end_dt=end_dt end_dt=end_dt
) )
candle_series = self.get_series_from_candles(
candles=candles,
start_dt=start_dt,
end_dt=end_dt,
data_frequency=frequency,
field=field,
)
df = pd.DataFrame(candle_series) series = dict()
for asset in candles:
asset_series = self.get_series_from_candles(
candles=candles[asset],
start_dt=start_dt,
end_dt=end_dt,
data_frequency=frequency,
field=field,
)
series[asset] = asset_series
df = pd.DataFrame(series)
df.dropna(inplace=True)
return df return df
def get_history_window(self, def get_history_window_with_bundle(self,
assets, assets,
end_dt, end_dt,
bar_count, bar_count,
frequency, frequency,
field, field,
data_frequency=None, data_frequency=None,
ffill=True): ffill=True,
force_auto_ingest=False):
""" """
Public API method that returns a dataframe containing the requested Public API method that returns a dataframe containing the requested
@@ -590,7 +623,8 @@ class Exchange:
end_dt=end_dt, end_dt=end_dt,
bar_count=adj_bar_count, bar_count=adj_bar_count,
field=field, field=field,
data_frequency=data_frequency data_frequency=data_frequency,
force_auto_ingest=force_auto_ingest
) )
except (PricingDataNotLoadedError, NoDataAvailableOnExchange): except (PricingDataNotLoadedError, NoDataAvailableOnExchange):
series = dict() series = dict()
+65 -45
View File
@@ -10,7 +10,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import os
import pickle import pickle
import signal import signal
import sys import sys
@@ -27,8 +26,6 @@ from catalyst.assets._assets import TradingPair
import catalyst.protocol as zp import catalyst.protocol as zp
from catalyst.algorithm import TradingAlgorithm from catalyst.algorithm import TradingAlgorithm
from catalyst.constants import LOG_LEVEL from catalyst.constants import LOG_LEVEL
from catalyst.data.minute_bars import BcolzMinuteBarWriter, \
BcolzMinuteBarReader
from catalyst.errors import OrderInBeforeTradingStart from catalyst.errors import OrderInBeforeTradingStart
from catalyst.exchange.exchange_blotter import ExchangeBlotter from catalyst.exchange.exchange_blotter import ExchangeBlotter
from catalyst.exchange.exchange_errors import ( from catalyst.exchange.exchange_errors import (
@@ -38,8 +35,8 @@ from catalyst.exchange.exchange_errors import (
OrphanOrderError) OrphanOrderError)
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \ from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
ExchangeLimitOrder, ExchangeStopOrder ExchangeLimitOrder, ExchangeStopOrder
from catalyst.exchange.exchange_utils import get_exchange_minute_writer_root, \ from catalyst.exchange.exchange_utils import save_algo_object, get_algo_object, \
save_algo_object, get_algo_object, get_algo_folder, get_algo_df, \ get_algo_folder, get_algo_df, \
save_algo_df save_algo_df
from catalyst.exchange.live_graph_clock import LiveGraphClock from catalyst.exchange.live_graph_clock import LiveGraphClock
from catalyst.exchange.simple_clock import SimpleClock from catalyst.exchange.simple_clock import SimpleClock
@@ -117,9 +114,12 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
else: else:
exchange = self.exchanges[exchange_name] exchange = self.exchanges[exchange_name]
data_frequency = self.data_frequency \
if self.sim_params.arena == 'backtest' else None
return self.asset_finder.lookup_symbol( return self.asset_finder.lookup_symbol(
symbol=symbol_str, symbol=symbol_str,
exchange=exchange, exchange=exchange,
data_frequency=data_frequency,
as_of_date=_lookup_date as_of_date=_lookup_date
) )
@@ -182,17 +182,19 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
# we want the key to be absent, not just empty # we want the key to be absent, not just empty
# Only include transactions for given dt # Only include transactions for given dt
stats['transactions'] = dict() stats['transactions'] = []
for date in period.processed_transactions: for date in period.processed_transactions:
if start_dt <= date < end_dt: if start_dt <= date < end_dt:
stats['transactions'][date] = \ transactions = period.processed_transactions[date]
period.processed_transactions[date] for t in transactions:
stats['transactions'].append(t.to_dict())
stats['orders'] = dict() stats['orders'] = []
for date in period.orders_by_modified: for date in period.orders_by_modified:
if start_dt <= date < end_dt: if start_dt <= date < end_dt:
stats['orders'][date] = \ orders = period.orders_by_modified[date]
period.orders_by_modified[date] for order in orders:
stats['orders'].append(orders[order].to_dict())
return stats return stats
@@ -201,6 +203,7 @@ class ExchangeTradingAlgorithmBacktest(ExchangeTradingAlgorithmBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ExchangeTradingAlgorithmBacktest, self).__init__(*args, **kwargs) super(ExchangeTradingAlgorithmBacktest, self).__init__(*args, **kwargs)
self.frame_stats = list()
self.blotter = ExchangeBlotter( self.blotter = ExchangeBlotter(
data_frequency=self.data_frequency, data_frequency=self.data_frequency,
# Default to NeverCancel in catalyst # Default to NeverCancel in catalyst
@@ -245,6 +248,42 @@ class ExchangeTradingAlgorithmBacktest(ExchangeTradingAlgorithmBase):
else: else:
return MarketOrder() return MarketOrder()
def is_last_frame_of_day(self, data):
# TODO: adjust here to support more intervals
next_frame_dt = data.current_dt + timedelta(minutes=1)
if next_frame_dt.date() > data.current_dt.date():
return True
else:
return False
def handle_data(self, data):
super(ExchangeTradingAlgorithmBacktest, self).handle_data(data)
if self.data_frequency == 'minute':
frame_stats = self.prepare_period_stats(
data.current_dt, data.current_dt + timedelta(minutes=1)
)
self.frame_stats.append(frame_stats)
def _create_stats_df(self):
stats = pd.DataFrame(self.frame_stats)
stats.set_index('period_close', inplace=True, drop=False)
return stats
def analyze(self, perf):
stats = self._create_stats_df() if self.data_frequency == 'minute' \
else perf
super(ExchangeTradingAlgorithmBacktest, self).analyze(stats)
def run(self, data=None, overwrite_sim_params=True):
perf = super(ExchangeTradingAlgorithmBacktest, self).run(
data, overwrite_sim_params
)
# Rebuilding the stats to support minute data
stats = self._create_stats_df() if self.data_frequency == 'minute' \
else perf
return stats
class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase): class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -252,7 +291,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
self.live_graph = kwargs.pop('live_graph', None) self.live_graph = kwargs.pop('live_graph', None)
self._clock = None self._clock = None
self.minute_stats = deque(maxlen=60) self.frame_stats = deque(maxlen=60)
self.pnl_stats = get_algo_df(self.algo_namespace, 'pnl_stats') self.pnl_stats = get_algo_df(self.algo_namespace, 'pnl_stats')
@@ -273,34 +312,11 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
self.stats_minutes = 5 self.stats_minutes = 5
super(ExchangeTradingAlgorithmLive, self).__init__(*args, **kwargs) super(ExchangeTradingAlgorithmLive, self).__init__(*args, **kwargs)
# TODO: fix precision before re-enabling
# self._create_minute_writer()
signal.signal(signal.SIGINT, self.signal_handler) signal.signal(signal.SIGINT, self.signal_handler)
log.info('initialized trading algorithm in live mode') log.info('initialized trading algorithm in live mode')
def _create_minute_writer(self):
root = get_exchange_minute_writer_root(self.exchange.name)
filename = os.path.join(root, 'metadata.json')
if os.path.isfile(filename):
writer = BcolzMinuteBarWriter.open(
root, self.sim_params.end_session)
else:
# TODO: need to be able to write more precise numbers
writer = BcolzMinuteBarWriter(
rootdir=root,
calendar=self.trading_calendar,
minutes_per_day=1440,
start_session=self.sim_params.start_session,
end_session=self.sim_params.end_session,
write_metadata=True
)
self.exchange.minute_writer = writer
self.exchange.minute_reader = BcolzMinuteBarReader(root)
def signal_handler(self, signal, frame): def signal_handler(self, signal, frame):
""" """
Handles the keyboard interruption signal. Handles the keyboard interruption signal.
@@ -544,8 +560,9 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
) )
self.exposure_stats = pd.concat([self.exposure_stats, df]) self.exposure_stats = pd.concat([self.exposure_stats, df])
save_algo_df(self.algo_namespace, 'exposure_stats', save_algo_df(
self.exposure_stats) self.algo_namespace, 'exposure_stats', self.exposure_stats
)
def handle_data(self, data): def handle_data(self, data):
""" """
@@ -562,8 +579,11 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
self._synchronize_portfolio() self._synchronize_portfolio()
transactions = self._check_open_orders() transactions = self._check_open_orders()
for transaction in transactions: if len(transactions) > 0:
self.perf_tracker.process_transaction(transaction) for transaction in transactions:
self.perf_tracker.process_transaction(transaction)
self.perf_tracker.update_performance()
if self._handle_data: if self._handle_data:
self._handle_data(self, data) self._handle_data(self, data)
@@ -578,22 +598,22 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
# Performance tracker and keep only minute and cumulative # Performance tracker and keep only minute and cumulative
self.perf_tracker.update_performance() self.perf_tracker.update_performance()
minute_stats = self.prepare_period_stats( frame_stats = self.prepare_period_stats(
data.current_dt, data.current_dt + timedelta(minutes=1)) data.current_dt, data.current_dt + timedelta(minutes=1))
# Saving the last hour in memory # Saving the last hour in memory
self.minute_stats.append(minute_stats) self.frame_stats.append(frame_stats)
self.add_pnl_stats(minute_stats) self.add_pnl_stats(frame_stats)
if self.recorded_vars: if self.recorded_vars:
self.add_custom_signals_stats(minute_stats) self.add_custom_signals_stats(frame_stats)
recorded_cols = list(self.recorded_vars.keys()) recorded_cols = list(self.recorded_vars.keys())
else: else:
recorded_cols = None recorded_cols = None
self.add_exposure_stats(minute_stats) self.add_exposure_stats(frame_stats)
print_df = pd.DataFrame(list(self.minute_stats)) print_df = pd.DataFrame(list(self.frame_stats))
log.info( log.info(
'statistics for the last {stats_minutes} minutes:\n{stats}'.format( 'statistics for the last {stats_minutes} minutes:\n{stats}'.format(
stats_minutes=self.stats_minutes, stats_minutes=self.stats_minutes,
+430 -197
View File
@@ -1,55 +1,37 @@
import os import os
import os
import shutil import shutil
from datetime import datetime, timedelta
from functools import partial
from itertools import chain from itertools import chain
from operator import is_not
import numpy as np
import pandas as pd import pandas as pd
import pytz
from catalyst.assets._assets import TradingPair from catalyst.assets._assets import TradingPair
from logbook import Logger from logbook import Logger
from pandas.tslib import Timestamp
from pytz import UTC from pytz import UTC
from six import itervalues from six import itervalues
from catalyst import get_calendar from catalyst import get_calendar
from catalyst.constants import DATE_TIME_FORMAT, AUTO_INGEST
from catalyst.constants import LOG_LEVEL from catalyst.constants import LOG_LEVEL
from catalyst.data.minute_bars import BcolzMinuteOverlappingData, \ from catalyst.data.minute_bars import BcolzMinuteOverlappingData, \
BcolzMinuteBarMetadata BcolzMinuteBarMetadata
from catalyst.exchange.bundle_utils import range_in_bundle, \ from catalyst.exchange.bundle_utils import range_in_bundle, \
get_bcolz_chunk, get_delta, get_month_start_end, \ get_bcolz_chunk, get_month_start_end, \
get_year_start_end, get_df_from_arrays, get_start_dt, get_period_label get_year_start_end, get_df_from_arrays, get_start_dt, get_period_label, \
get_delta, get_assets
from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \ from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \
BcolzExchangeBarWriter BcolzExchangeBarWriter
from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \ from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \
TempBundleNotFoundError, \ TempBundleNotFoundError, \
NoDataAvailableOnExchange, \ NoDataAvailableOnExchange, \
PricingDataNotLoadedError PricingDataNotLoadedError, DataCorruptionError, ExchangeSymbolsNotFound, \
from catalyst.exchange.exchange_utils import get_exchange_folder PricingDataValueError
from catalyst.utils.cli import maybe_show_progress from catalyst.exchange.exchange_utils import get_exchange_folder, \
from catalyst.utils.paths import ensure_directory get_exchange_symbols, save_exchange_symbols
import os
import shutil
from itertools import chain
import pandas as pd
from catalyst.assets._assets import TradingPair
from logbook import Logger
from pandas.tslib import Timestamp
from pytz import UTC
from six import itervalues
from catalyst import get_calendar
from catalyst.constants import LOG_LEVEL
from catalyst.data.minute_bars import BcolzMinuteOverlappingData, \
BcolzMinuteBarMetadata
from catalyst.exchange.bundle_utils import range_in_bundle, \
get_bcolz_chunk, get_delta, get_month_start_end, \
get_year_start_end, get_df_from_arrays, get_start_dt, get_period_label
from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \
BcolzExchangeBarWriter
from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \
TempBundleNotFoundError, \
NoDataAvailableOnExchange, \
PricingDataNotLoadedError
from catalyst.exchange.exchange_utils import get_exchange_folder
from catalyst.utils.cli import maybe_show_progress from catalyst.utils.cli import maybe_show_progress
from catalyst.utils.paths import ensure_directory from catalyst.utils.paths import ensure_directory
@@ -63,23 +45,14 @@ def _cachpath(symbol, type_):
class ExchangeBundle: class ExchangeBundle:
def __init__(self, exchange): def __init__(self, exchange_name):
self.exchange = exchange self.exchange_name = exchange_name
self.minutes_per_day = 1440 self.minutes_per_day = 1440
self.default_ohlc_ratio = 1000000 self.default_ohlc_ratio = 1000000
self._writers = dict() self._writers = dict()
self._readers = dict() self._readers = dict()
self.calendar = get_calendar('OPEN') self.calendar = get_calendar('OPEN')
self.exchange = None
def get_assets(self, include_symbols, exclude_symbols):
# TODO: filter exclude symbols assets
if include_symbols is not None:
include_symbols_list = include_symbols.split(',')
return self.exchange.get_assets(include_symbols_list)
else:
return self.exchange.get_assets()
def get_reader(self, data_frequency, path=None): def get_reader(self, data_frequency, path=None):
""" """
@@ -91,7 +64,7 @@ class ExchangeBundle:
""" """
if path is None: if path is None:
root = get_exchange_folder(self.exchange.name) root = get_exchange_folder(self.exchange_name)
path = BUNDLE_NAME_TEMPLATE.format( path = BUNDLE_NAME_TEMPLATE.format(
root=root, root=root,
frequency=data_frequency frequency=data_frequency
@@ -122,7 +95,7 @@ class ExchangeBundle:
BcolzMinuteBarWriter | BcolzDailyBarWriter BcolzMinuteBarWriter | BcolzDailyBarWriter
""" """
root = get_exchange_folder(self.exchange.name) root = get_exchange_folder(self.exchange_name)
path = BUNDLE_NAME_TEMPLATE.format( path = BUNDLE_NAME_TEMPLATE.format(
root=root, root=root,
frequency=data_frequency frequency=data_frequency
@@ -180,9 +153,9 @@ class ExchangeBundle:
---------- ----------
assets: list[TradingPair] assets: list[TradingPair]
The assets is scope. The assets is scope.
start_dt: datetime start_dt: pd.Timestamp
The chunk start date. The chunk start date.
end_dt: datetime end_dt: pd.Timestamp
The chunk end date. The chunk end date.
data_frequency: str data_frequency: str
@@ -231,8 +204,8 @@ class ExchangeBundle:
Parameters Parameters
---------- ----------
start_dt: datetime start_dt: pd.Timestamp
end_dt: datetime end_dt: pd.Timestamp
data_frequency: str data_frequency: str
Returns Returns
@@ -244,8 +217,91 @@ class ExchangeBundle:
if data_frequency == 'minute' \ if data_frequency == 'minute' \
else self.calendar.sessions_in_range(start_dt, end_dt) else self.calendar.sessions_in_range(start_dt, end_dt)
def _spot_empty_periods(self, ohlcv_df, asset, data_frequency,
empty_rows_behavior):
problems = []
nan_rows = ohlcv_df[ohlcv_df.isnull().T.any().T].index
if len(nan_rows) > 0:
dates = []
for row_date in nan_rows.values:
row_date = pd.to_datetime(row_date, utc=True)
if row_date > asset.start_date:
dates.append(row_date)
if len(dates) > 0:
end_dt = asset.end_minute if data_frequency == 'minute' \
else asset.end_daily
problem = '{name} ({start_dt} to {end_dt}) has empty ' \
'periods: {dates}'.format(
name=asset.symbol,
start_dt=asset.start_date.strftime(DATE_TIME_FORMAT),
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
dates=[date.strftime(DATE_TIME_FORMAT) for date in dates]
)
if empty_rows_behavior == 'warn':
log.warn(problem)
elif empty_rows_behavior == 'raise':
raise EmptyValuesInBundleError(
name=asset.symbol,
end_minute=end_dt,
dates=dates
)
else:
ohlcv_df.dropna(inplace=True)
else:
problem = None
problems.append(problem)
return problems
def _spot_duplicates(self, ohlcv_df, asset, data_frequency, threshold):
# TODO: work in progress
series = ohlcv_df.reset_index().groupby('close')['index'].apply(
np.array
)
ref_delta = timedelta(minutes=1) if data_frequency == 'minute' \
else timedelta(days=1)
dups = series.loc[lambda values: [len(x) > 10 for x in values]]
for index, dates in dups.iteritems():
prev_date = None
for date in dates:
if prev_date is not None:
delta = (date - prev_date) / 1e9
if delta == ref_delta.seconds:
log.info('pex')
prev_date = date
problems = []
for index, dates in dups.iteritems():
end_dt = asset.end_minute if data_frequency == 'minute' \
else asset.end_daily
problem = '{name} ({start_dt} to {end_dt}) has {threshold} ' \
'identical close values on: {dates}'.format(
name=asset.symbol,
start_dt=asset.start_date.strftime(DATE_TIME_FORMAT),
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
threshold=threshold,
dates=[pd.to_datetime(date).strftime(DATE_TIME_FORMAT)
for date in dates]
)
problems.append(problem)
return problems
def ingest_df(self, ohlcv_df, data_frequency, asset, writer, def ingest_df(self, ohlcv_df, data_frequency, asset, writer,
empty_rows_behavior='strip'): empty_rows_behavior='warn', duplicates_threshold=None):
""" """
Ingest a DataFrame of OHLCV data for a given market. Ingest a DataFrame of OHLCV data for a given market.
@@ -258,50 +314,16 @@ class ExchangeBundle:
empty_rows_behavior: str empty_rows_behavior: str
""" """
problems = []
if empty_rows_behavior is not 'ignore': if empty_rows_behavior is not 'ignore':
nan_rows = ohlcv_df[ohlcv_df.isnull().T.any().T].index problems += self._spot_empty_periods(
ohlcv_df, asset, data_frequency, empty_rows_behavior
)
if len(nan_rows) > 0: # if duplicates_threshold is not None:
dates = [] # problems += self._spot_duplicates(
previous_date = None # ohlcv_df, asset, data_frequency, duplicates_threshold
for row_date in nan_rows.values: # )
row_date = pd.to_datetime(row_date)
if previous_date is None:
dates.append(row_date)
else:
seq_date = previous_date + get_delta(1, data_frequency)
if row_date > seq_date:
dates.append(previous_date)
dates.append(row_date)
previous_date = row_date
dates.append(pd.to_datetime(nan_rows.values[-1]))
name = '{} from {} to {}'.format(
asset.symbol, ohlcv_df.index[0], ohlcv_df.index[-1]
)
if empty_rows_behavior == 'warn':
log.warn(
'\n{name} with end minute {end_minute} has empty rows '
'in ranges: {dates}'.format(
name=name,
end_minute=asset.end_minute,
dates=dates
)
)
elif empty_rows_behavior == 'raise':
raise EmptyValuesInBundleError(
name=name,
end_minute=asset.end_minute,
dates=dates
)
else:
ohlcv_df.dropna(inplace=True)
data = [] data = []
if not ohlcv_df.empty: if not ohlcv_df.empty:
@@ -310,8 +332,11 @@ class ExchangeBundle:
self._write(data, writer, data_frequency) self._write(data, writer, data_frequency)
return problems
def ingest_ctable(self, asset, data_frequency, period, def ingest_ctable(self, asset, data_frequency, period,
writer, empty_rows_behavior='strip', cleanup=False): writer, empty_rows_behavior='strip',
duplicates_threshold=100, cleanup=False):
""" """
Merge a ctable bundle chunk into the main bundle for the exchange. Merge a ctable bundle chunk into the main bundle for the exchange.
@@ -327,11 +352,17 @@ class ExchangeBundle:
cleanup: bool cleanup: bool
Remove the temp bundle directory after ingestion. Remove the temp bundle directory after ingestion.
:return: Returns
-------
list[str]
A list of problems which occurred during ingestion.
""" """
problems = []
# Download and extract the bundle # Download and extract the bundle
path = get_bcolz_chunk( path = get_bcolz_chunk(
exchange_name=self.exchange.name, exchange_name=self.exchange_name,
symbol=asset.symbol, symbol=asset.symbol,
data_frequency=data_frequency, data_frequency=data_frequency,
period=period period=period
@@ -375,12 +406,13 @@ class ExchangeBundle:
start_dt, end_dt, data_frequency start_dt, end_dt, data_frequency
) )
df = get_df_from_arrays(arrays, periods) df = get_df_from_arrays(arrays, periods)
self.ingest_df( problems += self.ingest_df(
ohlcv_df=df, ohlcv_df=df,
data_frequency=data_frequency, data_frequency=data_frequency,
asset=asset, asset=asset,
writer=writer, writer=writer,
empty_rows_behavior=empty_rows_behavior empty_rows_behavior=empty_rows_behavior,
duplicates_threshold=duplicates_threshold
) )
if cleanup: if cleanup:
@@ -390,7 +422,7 @@ class ExchangeBundle:
) )
shutil.rmtree(reader._rootdir) shutil.rmtree(reader._rootdir)
return reader._rootdir return filter(partial(is_not, None), problems)
def get_adj_dates(self, start, end, assets, data_frequency): def get_adj_dates(self, start, end, assets, data_frequency):
""" """
@@ -399,14 +431,14 @@ class ExchangeBundle:
Parameters Parameters
---------- ----------
start: datetime start: pd.Timestamp
end: datetime end: pd.Timestamp
assets: list[TradingPair] assets: list[TradingPair]
data_frequency: str data_frequency: str
Returns Returns
------- -------
datetime, datetime pd.Timestamp, pd.Timestamp
""" """
earliest_trade = None earliest_trade = None
last_entry = None last_entry = None
@@ -433,9 +465,10 @@ class ExchangeBundle:
start = earliest_trade start = earliest_trade
if end is None or (last_entry is not None and end > last_entry): if end is None or (last_entry is not None and end > last_entry):
end = last_entry end = last_entry.replace(minute=59, hour=23) \
if data_frequency == 'minute' else last_entry
if end is None or start is None or start >= end: if end is None or start is None or start > end:
raise NoDataAvailableOnExchange( raise NoDataAvailableOnExchange(
exchange=[asset.exchange for asset in assets], exchange=[asset.exchange for asset in assets],
symbol=[asset.symbol for asset in assets], symbol=[asset.symbol for asset in assets],
@@ -453,8 +486,8 @@ class ExchangeBundle:
---------- ----------
assets: list[TradingPair] assets: list[TradingPair]
data_frequency: str data_frequency: str
start_dt: datetime start_dt: pd.Timestamp
end_dt: datetime end_dt: pd.Timestamp
Returns Returns
------- -------
@@ -528,7 +561,8 @@ class ExchangeBundle:
return chunks return chunks
def ingest_assets(self, assets, data_frequency, start_dt=None, end_dt=None, def ingest_assets(self, assets, data_frequency, start_dt=None, end_dt=None,
show_progress=False, asset_chunks=False): show_progress=False, show_breakdown=False,
show_report=False):
""" """
Determine if data is missing from the bundle and attempt to ingest it. Determine if data is missing from the bundle and attempt to ingest it.
@@ -536,10 +570,10 @@ class ExchangeBundle:
---------- ----------
assets: list[TradingPair] assets: list[TradingPair]
data_frequency: str data_frequency: str
start_dt: datetime start_dt: pd.Timestamp
end_dt: datetime end_dt: pd.Timestamp
show_progress: bool show_progress: bool
asset_chunks: bool show_breakdown: bool
""" """
if start_dt is None: if start_dt is None:
@@ -562,22 +596,23 @@ class ExchangeBundle:
end_dt=end_dt end_dt=end_dt
) )
problems = []
# This is the common writer for the entire exchange bundle # This is the common writer for the entire exchange bundle
# we want to give an end_date far in time # we want to give an end_date far in time
writer = self.get_writer(start_dt, end_dt, data_frequency) writer = self.get_writer(start_dt, end_dt, data_frequency)
if asset_chunks: if show_breakdown:
for asset in chunks: for asset in chunks:
with maybe_show_progress( with maybe_show_progress(
chunks[asset], chunks[asset],
show_progress, show_progress,
label='Ingesting {frequency} price data for ' label='Ingesting {frequency} price data for '
'{symbol} on {exchange}'.format( '{symbol} on {exchange}'.format(
exchange=self.exchange.name, exchange=self.exchange_name,
frequency=data_frequency, frequency=data_frequency,
symbol=asset.symbol symbol=asset.symbol
)) as it: )) as it:
for chunk in it: for chunk in it:
self.ingest_ctable( problems += self.ingest_ctable(
asset=chunk['asset'], asset=chunk['asset'],
data_frequency=data_frequency, data_frequency=data_frequency,
period=chunk['period'], period=chunk['period'],
@@ -597,11 +632,11 @@ class ExchangeBundle:
show_progress, show_progress,
label='Ingesting {frequency} price data on ' label='Ingesting {frequency} price data on '
'{exchange}'.format( '{exchange}'.format(
exchange=self.exchange.name, exchange=self.exchange_name,
frequency=data_frequency, frequency=data_frequency,
)) as it: )) as it:
for chunk in it: for chunk in it:
self.ingest_ctable( problems += self.ingest_ctable(
asset=chunk['asset'], asset=chunk['asset'],
data_frequency=data_frequency, data_frequency=data_frequency,
period=chunk['period'], period=chunk['period'],
@@ -610,9 +645,144 @@ class ExchangeBundle:
cleanup=True cleanup=True
) )
if show_report and len(problems) > 0:
log.info('problems during ingestion:{}\n'.format(
'\n'.join(problems)
))
def ingest_csv(self, path, data_frequency, empty_rows_behavior='strip',
duplicates_threshold=100):
"""
Ingest price data from a CSV file.
Parameters
----------
path: str
data_frequency: str
Returns
-------
list[str]
A list of potential problems detected during ingestion.
"""
log.info('ingesting csv file: {}'.format(path))
try:
symbols_def = get_exchange_symbols(
self.exchange_name, is_local=True
)
except ExchangeSymbolsNotFound:
symbols_def = dict()
problems = []
df = pd.read_csv(
path,
header=0,
sep=',',
dtype=dict(
symbol=np.object_,
last_traded=np.object_,
open=np.float64,
high=np.float64,
close=np.float64,
volume=np.float64
),
parse_dates=['last_traded'],
index_col=None
)
min_start_dt = None
max_end_dt = None
symbols = df['symbol'].unique()
# Apply the timezone before creating an index for simplicity
df['last_traded'] = df['last_traded'].dt.tz_localize(pytz.UTC)
df.set_index(['symbol', 'last_traded'], drop=True, inplace=True)
assets = dict()
for symbol in symbols:
start_dt = df.index.get_level_values(1).min()
end_dt = df.index.get_level_values(1).max()
end_dt_key = 'end_{}'.format(data_frequency)
if symbol is symbols_def:
symbol_def = symbols_def[symbol]
start_dt = symbol_def['start_date'] \
if symbol_def['start_date'] < start_dt else start_dt
end_dt = symbol_def[end_dt_key] \
if symbol_def[end_dt_key] > end_dt else end_dt
end_daily = end_dt \
if data_frequency == 'daily' else symbol_def['end_daily']
end_minute = end_dt \
if data_frequency == 'minute' else symbol_def['end_minute']
else:
end_daily = end_dt if data_frequency == 'daily' else 'N/A'
end_minute = end_dt if data_frequency == 'minute' else 'N/A'
if min_start_dt is None or start_dt < min_start_dt:
min_start_dt = start_dt
if max_end_dt is None or end_dt > max_end_dt:
max_end_dt = end_dt
asset = TradingPair(
symbol=symbol,
exchange=self.exchange_name,
start_date=start_dt,
end_date=end_dt,
leverage=0, # TODO: add as an optional column
asset_name=symbol,
min_trade_size=0, # TODO: add as an optional column
end_daily=end_daily,
end_minute=end_minute,
exchange_symbol=symbol
)
assets[symbol] = asset
save_exchange_symbols(self.exchange_name, assets, True)
writer = self.get_writer(
start_dt=min_start_dt.replace(hour=00, minute=00),
end_dt=max_end_dt.replace(hour=23, minute=59),
data_frequency=data_frequency
)
for symbol in assets:
asset = assets[symbol]
ohlcv_df = df.loc[
(df.index.get_level_values(0) == symbol)
] # type: pd.DataFrame
ohlcv_df.index = ohlcv_df.index.droplevel(0)
period_start = start_dt.replace(hour=00, minute=00)
period_end = end_dt.replace(hour=23, minute=59)
periods = self.get_calendar_periods_range(
period_start, period_end, data_frequency
)
# We're not really resampling but ensuring that each frame
# contains data
ohlcv_df = ohlcv_df.reindex(periods, method='ffill')
ohlcv_df['volume'] = ohlcv_df['volume'].fillna(0)
problems += self.ingest_df(
ohlcv_df=ohlcv_df,
data_frequency=data_frequency,
asset=asset,
writer=writer,
empty_rows_behavior=empty_rows_behavior,
duplicates_threshold=duplicates_threshold
)
return filter(partial(is_not, None), problems)
def ingest(self, data_frequency, include_symbols=None, def ingest(self, data_frequency, include_symbols=None,
exclude_symbols=None, start=None, end=None, exclude_symbols=None, start=None, end=None, csv=None,
show_progress=True, environ=os.environ): show_progress=True, show_breakdown=True, show_report=True):
""" """
Inject data based on specified parameters. Inject data based on specified parameters.
@@ -621,17 +791,34 @@ class ExchangeBundle:
data_frequency: str data_frequency: str
include_symbols: str include_symbols: str
exclude_symbols: str exclude_symbols: str
start: datetime start: pd.Timestamp
end: datetime end: pd.Timestamp
show_progress: bool show_progress: bool
environ: environ:
""" """
assets = self.get_assets(include_symbols, exclude_symbols) if csv is not None:
self.ingest_csv(csv, data_frequency)
for frequency in data_frequency.split(','): else:
self.ingest_assets(assets, frequency, start, end, if self.exchange is None:
show_progress, True) # Avoid circular dependencies
from catalyst.exchange.factory import get_exchange
self.exchange = get_exchange(self.exchange_name)
assets = get_assets(
self.exchange, include_symbols, exclude_symbols
)
for frequency in data_frequency.split(','):
self.ingest_assets(
assets=assets,
data_frequency=frequency,
start_dt=start,
end_dt=end,
show_progress=show_progress,
show_breakdown=show_breakdown,
show_report=show_report
)
def get_history_window_series_and_load(self, def get_history_window_series_and_load(self,
assets, assets,
@@ -639,7 +826,9 @@ class ExchangeBundle:
bar_count, bar_count,
field, field,
data_frequency, data_frequency,
algo_end_dt=None algo_end_dt=None,
trailing_bar_count=None,
force_auto_ingest=False
): ):
""" """
Retrieve price data history, ingest missing data. Retrieve price data history, ingest missing data.
@@ -647,55 +836,69 @@ class ExchangeBundle:
Parameters Parameters
---------- ----------
assets: list[TradingPair] assets: list[TradingPair]
end_dt: datetime end_dt: pd.Timestamp
bar_count: int bar_count: int
field: str field: str
data_frequency: str data_frequency: str
algo_end_dt: datetime algo_end_dt: pd.Timestamp
Returns Returns
------- -------
Series Series
""" """
try: if AUTO_INGEST or force_auto_ingest:
try:
series = self.get_history_window_series(
assets=assets,
end_dt=end_dt,
bar_count=bar_count,
field=field,
data_frequency=data_frequency,
trailing_bar_count=trailing_bar_count,
)
return pd.DataFrame(series)
except PricingDataNotLoadedError:
start_dt = get_start_dt(end_dt, bar_count, data_frequency)
log.info(
'pricing data for {symbol} not found in range '
'{start} to {end}, updating the bundles.'.format(
symbol=[asset.symbol for asset in assets],
start=start_dt,
end=end_dt
)
)
self.ingest_assets(
assets=assets,
start_dt=start_dt,
end_dt=algo_end_dt, # TODO: apply trailing bars
data_frequency=data_frequency,
show_progress=True,
show_breakdown=True
)
series = self.get_history_window_series(
assets=assets,
end_dt=end_dt,
bar_count=bar_count,
field=field,
data_frequency=data_frequency,
reset_reader=True,
trailing_bar_count=trailing_bar_count,
)
return series
else:
series = self.get_history_window_series( series = self.get_history_window_series(
assets=assets, assets=assets,
end_dt=end_dt, end_dt=end_dt,
bar_count=bar_count, bar_count=bar_count,
field=field, field=field,
data_frequency=data_frequency data_frequency=data_frequency,
trailing_bar_count=trailing_bar_count,
) )
return pd.DataFrame(series) return pd.DataFrame(series)
except PricingDataNotLoadedError:
start_dt = get_start_dt(end_dt, bar_count, data_frequency)
log.info(
'pricing data for {symbol} not found in range '
'{start} to {end}, updating the bundles.'.format(
symbol=[asset.symbol for asset in assets],
start=start_dt,
end=end_dt
)
)
self.ingest_assets(
assets=assets,
start_dt=start_dt,
end_dt=algo_end_dt,
data_frequency=data_frequency,
show_progress=True,
asset_chunks=True
)
series = self.get_history_window_series(
assets=assets,
end_dt=end_dt,
bar_count=bar_count,
field=field,
data_frequency=data_frequency,
reset_reader=False
)
return series
def get_spot_values(self, def get_spot_values(self,
assets, assets,
field, field,
@@ -707,12 +910,18 @@ class ExchangeBundle:
The spot values for the gives assets, field and date. Reads from The spot values for the gives assets, field and date. Reads from
the exchange data bundle. the exchange data bundle.
:param assets: Parameters
:param field: ----------
:param dt: assets: list[TradingPair]
:param data_frequency: field: str
:param reset_reader: dt: pd.Timestamp
:return: data_frequency: str
reset_reader:
Returns
-------
float
""" """
values = [] values = []
try: try:
@@ -736,10 +945,12 @@ class ExchangeBundle:
raise PricingDataNotLoadedError( raise PricingDataNotLoadedError(
field=field, field=field,
first_trading_day=min([asset.start_date for asset in assets]), first_trading_day=min([asset.start_date for asset in assets]),
exchange=self.exchange.name, exchange=self.exchange_name,
symbols=symbols, symbols=symbols,
symbol_list=','.join(symbols), symbol_list=','.join(symbols),
data_frequency=data_frequency data_frequency=data_frequency,
start_dt=dt,
end_dt=dt
) )
def get_history_window_series(self, def get_history_window_series(self,
@@ -748,12 +959,20 @@ class ExchangeBundle:
bar_count, bar_count,
field, field,
data_frequency, data_frequency,
trailing_bar_count=None,
reset_reader=False): reset_reader=False):
start_dt = get_start_dt(end_dt, bar_count, data_frequency) start_dt = get_start_dt(end_dt, bar_count, data_frequency, False)
start_dt, end_dt = self.get_adj_dates( start_dt, _ = self.get_adj_dates(
start_dt, end_dt, assets, data_frequency start_dt, end_dt, assets, data_frequency
) )
if trailing_bar_count:
delta = get_delta(trailing_bar_count, data_frequency)
end_dt += delta
# This is an attempt to resolve some caching with the reader
# when auto-ingesting data.
# TODO: needs more work
reader = self.get_reader(data_frequency) reader = self.get_reader(data_frequency)
if reset_reader: if reset_reader:
del self._readers[reader._rootdir] del self._readers[reader._rootdir]
@@ -764,59 +983,69 @@ class ExchangeBundle:
raise PricingDataNotLoadedError( raise PricingDataNotLoadedError(
field=field, field=field,
first_trading_day=min([asset.start_date for asset in assets]), first_trading_day=min([asset.start_date for asset in assets]),
exchange=self.exchange.name, exchange=self.exchange_name,
symbols=symbols, symbols=symbols,
symbol_list=','.join(symbols), symbol_list=','.join(symbols),
data_frequency=data_frequency data_frequency=data_frequency,
start_dt=start_dt,
end_dt=end_dt
) )
series = dict()
for asset in assets: for asset in assets:
asset_start_dt, asset_end_dt = self.get_adj_dates( asset_start_dt, _ = self.get_adj_dates(
start_dt, end_dt, assets, data_frequency start_dt, end_dt, assets, data_frequency
) )
in_bundle = range_in_bundle( in_bundle = range_in_bundle(
asset, asset_start_dt, asset_end_dt, reader asset, asset_start_dt, end_dt, reader
) )
if not in_bundle: if not in_bundle:
raise PricingDataNotLoadedError( raise PricingDataNotLoadedError(
field=field, field=field,
first_trading_day=asset.start_date, first_trading_day=asset.start_date,
exchange=self.exchange.name, exchange=self.exchange_name,
symbols=asset.symbol, symbols=asset.symbol,
symbol_list=asset.symbol, symbol_list=asset.symbol,
data_frequency=data_frequency data_frequency=data_frequency,
start_dt=asset_start_dt,
end_dt=end_dt
) )
series = dict() periods = self.get_calendar_periods_range(
try: asset_start_dt, end_dt, data_frequency
)
# This does not behave well when requesting multiple assets
# when the start or end date of one asset is outside of the range
# looking at the logic in load_raw_arrays(), we are not achieving
# any performance gain by requesting multiple sids at once. It's
# looping through the sids and making separate requests anyway.
arrays = reader.load_raw_arrays( arrays = reader.load_raw_arrays(
sids=[asset.sid for asset in assets], sids=[asset.sid],
fields=[field], fields=[field],
start_dt=start_dt, start_dt=start_dt,
end_dt=end_dt end_dt=end_dt
) )
if len(arrays) == 0:
raise DataCorruptionError(
exchange=self.exchange_name,
symbols=asset.symbol,
start_dt=asset_start_dt,
end_dt=end_dt
)
except Exception: field_values = arrays[0][:, 0]
symbols = [asset.symbol.encode('utf-8') for asset in assets]
raise PricingDataNotLoadedError(
field=field,
first_trading_day=min([asset.start_date for asset in assets]),
exchange=self.exchange.name,
symbols=symbols,
symbol_list=','.join(symbols),
data_frequency=data_frequency
)
periods = self.get_calendar_periods_range( try:
start_dt, end_dt, data_frequency value_series = pd.Series(field_values, index=periods)
) series[asset] = value_series
except ValueError as e:
for asset_index, asset in enumerate(assets): raise PricingDataValueError(
asset_values = arrays[asset_index] exchange=asset.exchange,
symbol=asset.symbol,
value_series = pd.Series(asset_values.flatten(), index=periods) start_dt=asset_start_dt,
series[asset] = value_series end_dt=end_dt,
error=e
)
return series return series
@@ -830,14 +1059,18 @@ class ExchangeBundle:
""" """
log.debug('cleaning exchange {}, frequency {}'.format( log.debug('cleaning exchange {}, frequency {}'.format(
self.exchange.name, data_frequency self.exchange_name, data_frequency
)) ))
root = get_exchange_folder(self.exchange.name) root = get_exchange_folder(self.exchange_name)
symbols = os.path.join(root, 'symbols.json') symbols = os.path.join(root, 'symbols.json')
if os.path.isfile(symbols): if os.path.isfile(symbols):
os.remove(symbols) os.remove(symbols)
local_symbols = os.path.join(root, 'symbols_local.json')
if os.path.isfile(local_symbols):
os.remove(local_symbols)
temp_bundles = os.path.join(root, 'temp_bundles') temp_bundles = os.path.join(root, 'temp_bundles')
if os.path.isdir(temp_bundles): if os.path.isdir(temp_bundles):
+52 -47
View File
@@ -6,7 +6,7 @@ import pandas as pd
from catalyst.assets._assets import TradingPair from catalyst.assets._assets import TradingPair
from logbook import Logger from logbook import Logger
from catalyst.constants import LOG_LEVEL from catalyst.constants import LOG_LEVEL, AUTO_INGEST
from catalyst.data.data_portal import DataPortal from catalyst.data.data_portal import DataPortal
from catalyst.exchange.exchange_bundle import ExchangeBundle from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.exchange_errors import ( from catalyst.exchange.exchange_errors import (
@@ -21,7 +21,6 @@ log = Logger('DataPortalExchange', level=LOG_LEVEL)
class DataPortalExchangeBase(DataPortal): class DataPortalExchangeBase(DataPortal):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.exchanges = kwargs.pop('exchanges', None)
# TODO: put somewhere accessible by each algo # TODO: put somewhere accessible by each algo
self.retry_get_history_window = 5 self.retry_get_history_window = 5
self.retry_get_spot_value = 5 self.retry_get_spot_value = 5
@@ -49,11 +48,10 @@ class DataPortalExchangeBase(DataPortal):
if len(exchange_assets) > 1: if len(exchange_assets) > 1:
df_list = [] df_list = []
for exchange_name in exchange_assets: for exchange_name in exchange_assets:
exchange = self.exchanges[exchange_name]
assets = exchange_assets[exchange_name] assets = exchange_assets[exchange_name]
df_exchange = self.get_exchange_history_window( df_exchange = self.get_exchange_history_window(
exchange, exchange_name,
assets, assets,
end_dt, end_dt,
bar_count, bar_count,
@@ -68,9 +66,9 @@ class DataPortalExchangeBase(DataPortal):
return pd.concat(df_list) return pd.concat(df_list)
else: else:
exchange = self.exchanges[list(exchange_assets.keys())[0]] exchange_name = list(exchange_assets.keys())[0]
return self.get_exchange_history_window( return self.get_exchange_history_window(
exchange, exchange_name,
assets, assets,
end_dt, end_dt,
bar_count, bar_count,
@@ -122,7 +120,7 @@ class DataPortalExchangeBase(DataPortal):
@abc.abstractmethod @abc.abstractmethod
def get_exchange_history_window(self, def get_exchange_history_window(self,
exchange, exchange_name,
assets, assets,
end_dt, end_dt,
bar_count, bar_count,
@@ -136,9 +134,8 @@ class DataPortalExchangeBase(DataPortal):
attempt_index=0): attempt_index=0):
try: try:
if isinstance(assets, TradingPair): if isinstance(assets, TradingPair):
exchange = self.exchanges[assets.exchange]
spot_values = self.get_exchange_spot_value( spot_values = self.get_exchange_spot_value(
exchange, [assets], field, dt, data_frequency) assets.exchange, [assets], field, dt, data_frequency)
if not spot_values: if not spot_values:
return np.nan return np.nan
@@ -154,17 +151,16 @@ class DataPortalExchangeBase(DataPortal):
exchange_assets[asset.exchange].append(asset) exchange_assets[asset.exchange].append(asset)
if len(list(exchange_assets.keys())) == 1: if len(list(exchange_assets.keys())) == 1:
exchange = self.exchanges[list(exchange_assets.keys())[0]] exchange_name = list(exchange_assets.keys())[0]
return self.get_exchange_spot_value( return self.get_exchange_spot_value(
exchange, assets, field, dt, data_frequency) exchange_name, assets, field, dt, data_frequency)
else: else:
spot_values = [] spot_values = []
for exchange_name in exchange_assets: for exchange_name in exchange_assets:
exchange = self.exchanges[exchange_name]
assets = exchange_assets[exchange_name] assets = exchange_assets[exchange_name]
exchange_spot_values = self.get_exchange_spot_value( exchange_spot_values = self.get_exchange_spot_value(
exchange, exchange_name,
assets, assets,
field, field,
dt, dt,
@@ -199,7 +195,7 @@ class DataPortalExchangeBase(DataPortal):
return self._get_spot_value(assets, field, dt, data_frequency) return self._get_spot_value(assets, field, dt, data_frequency)
@abc.abstractmethod @abc.abstractmethod
def get_exchange_spot_value(self, exchange, assets, field, dt, def get_exchange_spot_value(self, exchange_name, assets, field, dt,
data_frequency): data_frequency):
return return
@@ -214,10 +210,11 @@ class DataPortalExchangeBase(DataPortal):
class DataPortalExchangeLive(DataPortalExchangeBase): class DataPortalExchangeLive(DataPortalExchangeBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.exchanges = kwargs.pop('exchanges', None)
super(DataPortalExchangeLive, self).__init__(*args, **kwargs) super(DataPortalExchangeLive, self).__init__(*args, **kwargs)
def get_exchange_history_window(self, def get_exchange_history_window(self,
exchange, exchange_name,
assets, assets,
end_dt, end_dt,
bar_count, bar_count,
@@ -230,7 +227,7 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
Parameters Parameters
---------- ----------
exchange: Exchange exchange_name: Exchange
assets: list[TradingPair] assets: list[TradingPair]
end_dt: datetime end_dt: datetime
bar_count: int bar_count: int
@@ -244,6 +241,7 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
DataFrame DataFrame
""" """
exchange = self.exchanges[exchange_name]
df = exchange.get_history_window( df = exchange.get_history_window(
assets, assets,
end_dt, end_dt,
@@ -254,14 +252,14 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
ffill) ffill)
return df return df
def get_exchange_spot_value(self, exchange, assets, field, dt, def get_exchange_spot_value(self, exchange_name, assets, field, dt,
data_frequency): data_frequency):
""" """
A spot value for the exchange. A spot value for the exchange.
Parameters Parameters
---------- ----------
exchange: Exchange exchange_name: str
assets: list[TradingPair] assets: list[TradingPair]
field: str field: str
dt: datetime dt: datetime
@@ -272,6 +270,7 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
float float
""" """
exchange = self.exchanges[exchange_name]
exchange_spot_values = exchange.get_spot_value( exchange_spot_values = exchange.get_spot_value(
assets, field, dt, data_frequency) assets, field, dt, data_frequency)
@@ -280,16 +279,16 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
class DataPortalExchangeBacktest(DataPortalExchangeBase): class DataPortalExchangeBacktest(DataPortalExchangeBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.exchange_names = kwargs.pop('exchange_names', None)
super(DataPortalExchangeBacktest, self).__init__(*args, **kwargs) super(DataPortalExchangeBacktest, self).__init__(*args, **kwargs)
self.exchange_bundles = dict() self.exchange_bundles = dict()
self.history_loaders = dict() self.history_loaders = dict()
self.minute_history_loaders = dict() self.minute_history_loaders = dict()
for exchange_name in self.exchanges: for name in self.exchange_names:
exchange = self.exchanges[exchange_name] self.exchange_bundles[name] = ExchangeBundle(name)
self.exchange_bundles[exchange_name] = ExchangeBundle(exchange)
def _get_first_trading_day(self, assets): def _get_first_trading_day(self, assets):
first_date = None first_date = None
@@ -299,7 +298,7 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
return first_date return first_date
def get_exchange_history_window(self, def get_exchange_history_window(self,
exchange, exchange_name,
assets, assets,
end_dt, end_dt,
bar_count, bar_count,
@@ -326,12 +325,13 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
DataFrame DataFrame
""" """
bundle = self.exchange_bundles[exchange.name] # type: ExchangeBundle bundle = self.exchange_bundles[exchange_name] # type: ExchangeBundle
freq, candle_size, unit, adj_data_frequency = get_frequency( freq, candle_size, unit, adj_data_frequency = get_frequency(
frequency, data_frequency frequency, data_frequency
) )
adj_bar_count = candle_size * bar_count adj_bar_count = candle_size * bar_count
trailing_bar_count = candle_size - 1
if data_frequency == 'minute' and adj_data_frequency == 'daily': if data_frequency == 'minute' and adj_data_frequency == 'daily':
end_dt = end_dt.floor('1D') end_dt = end_dt.floor('1D')
@@ -343,13 +343,14 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
field=field, field=field,
data_frequency=adj_data_frequency, data_frequency=adj_data_frequency,
algo_end_dt=self._last_available_session, algo_end_dt=self._last_available_session,
trailing_bar_count=trailing_bar_count
) )
df = resample_history_df(pd.DataFrame(series), freq, field) df = resample_history_df(pd.DataFrame(series), freq, field)
return df return df
def get_exchange_spot_value(self, def get_exchange_spot_value(self,
exchange, exchange_name,
assets, assets,
field, field,
dt, dt,
@@ -361,7 +362,7 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
Parameters Parameters
---------- ----------
exchange: Exchange exchange_name: str
assets: list[TradingPair] assets: list[TradingPair]
field: str field: str
dt: datetime dt: datetime
@@ -372,30 +373,34 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
float float
""" """
bundle = self.exchange_bundles[exchange.name] bundle = self.exchange_bundles[exchange_name]
if data_frequency == 'daily': if data_frequency == 'daily':
dt = dt.floor('1D') dt = dt.floor('1D')
else: else:
dt = dt.floor('1 min') dt = dt.floor('1 min')
try: if AUTO_INGEST:
return bundle.get_spot_values(assets, field, dt, data_frequency) try:
return bundle.get_spot_values(
except PricingDataNotLoadedError: assets, field, dt, data_frequency
log.info(
'pricing data for {symbol} not found on {dt}'
', updating the bundles.'.format(
symbol=[asset.symbol for asset in assets],
dt=dt
) )
) except PricingDataNotLoadedError:
bundle.ingest_assets( log.info(
assets=assets, 'pricing data for {symbol} not found on {dt}'
start_dt=self._first_trading_day, ', updating the bundles.'.format(
end_dt=self._last_available_session, symbol=[asset.symbol for asset in assets],
data_frequency=data_frequency, dt=dt
show_progress=True )
) )
return bundle.get_spot_values( bundle.ingest_assets(
assets, field, dt, data_frequency, True assets=assets,
) start_dt=self._first_trading_day,
end_dt=self._last_available_session,
data_frequency=data_frequency,
show_progress=True
)
return bundle.get_spot_values(
assets, field, dt, data_frequency, True
)
else:
return bundle.get_spot_values(assets, field, dt, data_frequency)
+18 -6
View File
@@ -211,12 +211,24 @@ class PricingDataBeforeTradingError(ZiplineError):
class PricingDataNotLoadedError(ZiplineError): class PricingDataNotLoadedError(ZiplineError):
msg = ('Pricing data {field} for trading pairs {symbols} trading on ' msg = ('Missing data for {exchange} {symbols} in date range '
'exchange {exchange} since {first_trading_day} is unavailable. ' '[{start_dt} - {end_dt}]'
'The bundle data is either out-of-date or has not been loaded yet. ' '\nPlease run: `catalyst ingest-exchange -x {exchange} -f '
'Please ingest data using the command ' '{data_frequency} -i {symbol_list}`. See catalyst documentation '
'`catalyst ingest-exchange -x {exchange} -f {data_frequency} -i {symbol_list}`. ' 'for details.').strip()
'See catalyst documentation for details.').strip()
class PricingDataValueError(ZiplineError):
msg = ('Unable to retrieve pricing data for {exchange} {symbol} '
'[{start_dt} - {end_dt}]: {error}').strip()
class DataCorruptionError(ZiplineError):
msg = ('Unable to validate data for {exchange} {symbols} in date range '
'[{start_dt} - {end_dt}]. The data is either corrupted or '
'unavailable. Please try deleting this bundle:'
'\n`catalyst clean-exchange -x {exchange}\n'
'Then, ingest the data again. Please contact the Catalyst team if '
'the issue persists.').strip()
class ApiCandlesError(ZiplineError): class ApiCandlesError(ZiplineError):
+102 -13
View File
@@ -1,20 +1,40 @@
import hashlib
import json import json
import os import os
import pickle import pickle
import re import re
import shutil
from datetime import date, datetime from datetime import date, datetime
import pandas as pd import pandas as pd
from catalyst.assets._assets import TradingPair from catalyst.assets._assets import TradingPair
from six.moves.urllib import request from six.moves.urllib import request
from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound, \ from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound, \
InvalidHistoryFrequencyError, InvalidHistoryFrequencyAlias InvalidHistoryFrequencyError, InvalidHistoryFrequencyAlias
from catalyst.utils.paths import data_root, ensure_directory, \ from catalyst.utils.paths import data_root, ensure_directory, \
last_modified_time last_modified_time
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
'{exchange}/symbols.json' def get_sid(symbol):
"""
Create a sid by hashing the symbol of a currency pair.
Parameters
----------
symbol: str
Returns
-------
int
The resulting sid.
"""
sid = int(
hashlib.sha256(symbol.encode('utf-8')).hexdigest(), 16
) % 10 ** 6
return sid
def get_exchange_folder(exchange_name, environ=None): def get_exchange_folder(exchange_name, environ=None):
@@ -41,7 +61,7 @@ def get_exchange_folder(exchange_name, environ=None):
return exchange_folder return exchange_folder
def get_exchange_symbols_filename(exchange_name, environ=None): def get_exchange_symbols_filename(exchange_name, is_local=False, environ=None):
""" """
The absolute path of the exchange's symbol.json file. The absolute path of the exchange's symbol.json file.
@@ -55,8 +75,9 @@ def get_exchange_symbols_filename(exchange_name, environ=None):
str str
""" """
name = 'symbols.json' if not is_local else 'symbols_local.json'
exchange_folder = get_exchange_folder(exchange_name, environ) exchange_folder = get_exchange_folder(exchange_name, environ)
return os.path.join(exchange_folder, 'symbols.json') return os.path.join(exchange_folder, name)
def download_exchange_symbols(exchange_name, environ=None): def download_exchange_symbols(exchange_name, environ=None):
@@ -79,13 +100,14 @@ def download_exchange_symbols(exchange_name, environ=None):
return response return response
def get_exchange_symbols(exchange_name, environ=None): def get_exchange_symbols(exchange_name, is_local=False, environ=None):
""" """
The de-serialized content of the exchange's symbols.json. The de-serialized content of the exchange's symbols.json.
Parameters Parameters
---------- ----------
exchange_name: str exchange_name: str
is_local: bool
environ: environ:
Returns Returns
@@ -93,18 +115,21 @@ def get_exchange_symbols(exchange_name, environ=None):
Object Object
""" """
filename = get_exchange_symbols_filename(exchange_name) filename = get_exchange_symbols_filename(exchange_name, is_local)
if not os.path.isfile(filename) or \ if not is_local and (not os.path.isfile(filename) or pd.Timedelta(
pd.Timedelta(pd.Timestamp('now', pd.Timestamp('now', tz='UTC') - last_modified_time(
tz='UTC') - last_modified_time( filename)).days > 1):
filename)).days > 1:
download_exchange_symbols(exchange_name, environ) download_exchange_symbols(exchange_name, environ)
if os.path.isfile(filename): if os.path.isfile(filename):
with open(filename) as data_file: with open(filename) as data_file:
data = json.load(data_file) try:
return data data = json.load(data_file)
return data
except ValueError:
return dict()
else: else:
raise ExchangeSymbolsNotFound( raise ExchangeSymbolsNotFound(
exchange=exchange_name, exchange=exchange_name,
@@ -112,6 +137,32 @@ def get_exchange_symbols(exchange_name, environ=None):
) )
def save_exchange_symbols(exchange_name, assets, is_local=False, environ=None):
"""
Save assets into an exchange_symbols file.
Parameters
----------
exchange_name: str
assets: list[dict[str, object]]
is_local: bool
environ
Returns
-------
"""
asset_dicts = dict()
for symbol in assets:
asset_dicts[symbol] = assets[symbol].to_dict()
filename = get_exchange_symbols_filename(
exchange_name, is_local, environ
)
with open(filename, 'wt') as handle:
json.dump(asset_dicts, handle, indent=4, default=symbols_serial)
def get_symbols_string(assets): def get_symbols_string(assets):
""" """
A concatenated string of symbols from a list of assets. A concatenated string of symbols from a list of assets.
@@ -158,6 +209,24 @@ def get_exchange_auth(exchange_name, environ=None):
return data return data
def delete_algo_folder(algo_name, environ=None):
"""
Delete the folder containing the algo state.
Parameters
----------
algo_name: str
environ:
Returns
-------
str
"""
folder = get_algo_folder(algo_name, environ)
shutil.rmtree(folder)
def get_algo_folder(algo_name, environ=None): def get_algo_folder(algo_name, environ=None):
""" """
The algorithm root folder of the algorithm. The algorithm root folder of the algorithm.
@@ -344,6 +413,25 @@ def get_exchange_bundles_folder(exchange_name, environ=None):
return temp_bundles return temp_bundles
def symbols_serial(obj):
"""
JSON serializer for objects not serializable by default json code
Parameters
----------
obj: Object
Returns
-------
str
"""
if isinstance(obj, (datetime, date)):
return obj.floor('1D').strftime(DATE_FORMAT)
raise TypeError("Type %s not serializable" % type(obj))
def perf_serial(obj): def perf_serial(obj):
""" """
JSON serializer for objects not serializable by default json code JSON serializer for objects not serializable by default json code
@@ -481,4 +569,5 @@ def resample_history_df(df, freq, field):
else: else:
raise ValueError('Invalid field.') raise ValueError('Invalid field.')
return df.resample(freq).agg(agg) resampled_df = df.resample(freq).agg(agg)
return resampled_df
+9 -5
View File
@@ -35,8 +35,13 @@ class Poloniex(Exchange):
def __init__(self, key, secret, base_currency, portfolio=None): def __init__(self, key, secret, base_currency, portfolio=None):
self.api = Poloniex_api(key=key, secret=secret) self.api = Poloniex_api(key=key, secret=secret)
self.name = 'poloniex' self.name = 'poloniex'
self.assets = {}
self.assets = dict()
self.load_assets() self.load_assets()
self.local_assets = dict()
self.load_assets(is_local=True)
self.base_currency = base_currency self.base_currency = base_currency
self._portfolio = portfolio self._portfolio = portfolio
self.minute_writer = None self.minute_writer = None
@@ -47,7 +52,7 @@ class Poloniex(Exchange):
self.max_requests_per_minute = 60 self.max_requests_per_minute = 60
self.request_cpt = dict() self.request_cpt = dict()
self.bundle = ExchangeBundle(self) self.bundle = ExchangeBundle(self.name)
def sanitize_curency_symbol(self, exchange_symbol): def sanitize_curency_symbol(self, exchange_symbol):
""" """
@@ -226,10 +231,9 @@ class Poloniex(Exchange):
ohlc_map = dict() ohlc_map = dict()
for asset in asset_list: for asset in asset_list:
delta = end_dt - pd.to_datetime('1970-1-1', utc=True)
end = int(delta.total_seconds())
# TODO: what's wrong with this?
# end = int(time.mktime(end_dt.timetuple()))
end = int(time.time())
if bar_count is None: if bar_count is None:
start = end - 2 * frequency start = end - 2 * frequency
else: else:
+4 -2
View File
@@ -3,6 +3,7 @@ import json
import time import time
import hmac import hmac
import hashlib import hashlib
import ssl
from six.moves import urllib from six.moves import urllib
@@ -104,9 +105,10 @@ class Poloniex_api(object):
req = urllib.request.Request( req = urllib.request.Request(
url, url,
data=post_data, data=post_data,
headers=headers headers=headers,
) )
return json.loads(urlopen(req).read()) return json.loads(
urlopen(req, context=ssl._create_unverified_context()).read())
def returnticker(self): def returnticker(self):
return self.query('returnTicker', {}) return self.query('returnTicker', {})
+102 -12
View File
@@ -1,7 +1,19 @@
import numbers
import numpy as np import numpy as np
import pandas as pd import pandas as pd
def trend_direction(series):
if series[-1] is np.nan or series[-1] is np.nan:
return None
if series[-1] > series[-2]:
return 'up'
else:
return 'down'
def crossover(source, target): def crossover(source, target):
""" """
The `x`-series is defined as having crossed over `y`-series if the value The `x`-series is defined as having crossed over `y`-series if the value
@@ -18,14 +30,25 @@ def crossover(source, target):
bool bool
""" """
if source[-1] is np.nan or source[-2] is np.nan \ if isinstance(target, numbers.Number):
or target[-1] is np.nan or target[-2] is np.nan: if source[-1] is np.nan or source[-2] is np.nan \
return False or target is np.nan:
return False
if source[-1] >= target > source[-2]:
return True
else:
return False
if source[-1] > target[-1] and source[-2] < target[-2]:
return True
else: 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 crossunder(source, target): def crossunder(source, target):
@@ -44,14 +67,56 @@ def crossunder(source, target):
bool bool
""" """
if source[-1] is np.nan or source[-2] is np.nan \ if isinstance(target, numbers.Number):
or target[-1] is np.nan or target[-2] is np.nan: if source[-1] is np.nan or source[-2] is np.nan \
return False or target is np.nan:
return False
if source[-1] < target[-1] and source[-2] > target[-2]: if source[-1] < target <= source[-2]:
return True return True
else:
return False
else: 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):
"""
Volume-weighted average price (VWAP) is a ratio generally used by
institutional investors and mutual funds to make buys and sells so as not
to disturb the market prices with large orders. It is the average share
price of a stock weighted against its trading volume within a particular
time frame, generally one day.
Read more: Volume Weighted Average Price - VWAP
https://www.investopedia.com/terms/v/vwap.asp#ixzz4xt922daE
Parameters
----------
df: pd.DataFrame
Returns
-------
"""
if 'close' not in df.columns or 'volume' not in df.columns:
raise ValueError('price data must include `volume` and `close`')
vol_sum = np.nansum(df['volume'].values)
try:
ret = np.nansum(df['close'].values * df['volume'].values) / vol_sum
except ZeroDivisionError:
ret = np.nan
return ret
def get_pretty_stats(stats_df, recorded_cols=None, num_rows=10): def get_pretty_stats(stats_df, recorded_cols=None, num_rows=10):
@@ -129,3 +194,28 @@ def df_to_string(df):
pd.set_option('display.max_colwidth', 1000) pd.set_option('display.max_colwidth', 1000)
return df.to_string() 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 = pd.DataFrame(all_trans)
if not transactions.empty:
transactions.set_index('dt', inplace=True, drop=True)
return transactions
+142
View File
@@ -0,0 +1,142 @@
import os
import tempfile
import pandas as pd
import six
from catalyst.assets._assets import TradingPair, get_calendar
from logbook import Logger
from pandas.util.testing import assert_frame_equal
from catalyst.constants import LOG_LEVEL
from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
from catalyst.exchange.exchange_data_portal import DataPortalExchangeBacktest
from catalyst.exchange.factory import get_exchanges
from catalyst.utils.paths import ensure_directory
log = Logger('Validator', level=LOG_LEVEL)
def output_df(df, assets, name=None):
"""
Outputs a price DataFrame to a temp folder.
Parameters
----------
df: pd.DataFrame
assets
name
Returns
-------
"""
if isinstance(assets, TradingPair):
exchange_folder = assets.exchange
asset_folder = assets.symbol
else:
exchange_folder = ','.join([asset.exchange for asset in assets])
asset_folder = ','.join([asset.symbol for asset in assets])
folder = os.path.join(
tempfile.gettempdir(), 'catalyst', exchange_folder, asset_folder
)
ensure_directory(folder)
if name is None:
name = 'output'
path = os.path.join(folder, '{}.csv'.format(name))
df.to_csv(path)
return path
class Validator(object):
def __init__(self, data_portal):
self.data_portal = data_portal
def compare_bundle_with_exchange(self, exchange, assets, end_dt, bar_count,
sample_minutes):
"""
Creates DataFrames from the bundle and exchange for the specified
data set.
Parameters
----------
exchange: Exchange
assets
end_dt
bar_count
sample_minutes
Returns
-------
"""
freq = '{}T'.format(sample_minutes)
log.info('creating data sample from bundle')
df1 = self.data_portal.get_history_window(
assets=assets,
end_dt=end_dt,
bar_count=bar_count,
frequency=freq,
field='close',
data_frequency='minute'
)
path = output_df(df1, assets, '{}_resampled'.format(freq))
log.info('saved resampled bundle candles: {}\n{}'.format(
path, df1.tail(10))
)
log.info('creating data sample from exchange api')
candles = exchange.get_candles(
end_dt=end_dt,
freq='{}T'.format(sample_minutes),
assets=assets,
bar_count=bar_count
)
series = dict()
for asset in assets:
series[asset] = pd.Series(
data=[candle['close'] for candle in candles[asset]],
index=[candle['last_traded'] for candle in candles[asset]]
)
df2 = pd.DataFrame(series)
path = output_df(df2, assets, '{}_api'.format(freq))
log.info('saved exchange api candles: {}\n{}'.format(
path, df2.tail(10))
)
try:
assert_frame_equal(df1, df2)
return True
except:
log.warn('differences found in dataframes')
return False
if __name__ == '__main__':
exchanges = get_exchanges(['poloniex'])
exchange = six.next(six.itervalues(exchanges))
assets = exchange.get_assets(symbols=['eth_btc'])
open_calendar = get_calendar('OPEN')
asset_finder = AssetFinderExchange()
data_portal = DataPortalExchangeBacktest(
exchanges=exchanges,
asset_finder=asset_finder,
trading_calendar=open_calendar,
first_trading_day=None # will set dynamically based on assets
)
validator = Validator(data_portal=data_portal)
validator.compare_bundle_with_exchange(
exchange=exchange,
assets=assets,
end_dt=pd.to_datetime('2017-11-10 1:00', utc=True),
bar_count=200,
sample_minutes=30
)
+14 -22
View File
@@ -77,6 +77,7 @@ class LimitOrder(ExecutionStyle):
Execution style representing an order to be executed at a price equal to or Execution style representing an order to be executed at a price equal to or
better than a specified limit price. better than a specified limit price.
""" """
def __init__(self, limit_price, exchange=None): def __init__(self, limit_price, exchange=None):
""" """
Store the given price. Store the given price.
@@ -99,6 +100,7 @@ class StopOrder(ExecutionStyle):
Execution style representing an order to be placed once the market price Execution style representing an order to be placed once the market price
reaches a specified stop price. reaches a specified stop price.
""" """
def __init__(self, stop_price, exchange=None): def __init__(self, stop_price, exchange=None):
""" """
Store the given price. Store the given price.
@@ -121,6 +123,7 @@ class StopLimitOrder(ExecutionStyle):
Execution style representing a limit order to be placed with a specified Execution style representing a limit order to be placed with a specified
limit price once the market reaches a specified stop price. limit price once the market reaches a specified stop price.
""" """
def __init__(self, limit_price, stop_price, exchange=None): def __init__(self, limit_price, stop_price, exchange=None):
""" """
Store the given prices Store the given prices
@@ -144,31 +147,20 @@ class StopLimitOrder(ExecutionStyle):
def asymmetric_round_price_to_penny(price, prefer_round_down, def asymmetric_round_price_to_penny(price, prefer_round_down,
diff=(0.0095 - .005)): diff=(0.0095 - .005)):
""" """
Asymmetric rounding function for adjusting prices to two places in a way Modified the original function because we do not want to round
that "improves" the price. For limit prices, this means preferring to prices on crypto exchange.
round down on buys and preferring to round up on sells. For stop prices,
it means the reverse.
If prefer_round_down == True: Parameters
When .05 below to .95 above a penny, use that penny. ----------
If prefer_round_down == False: price: float
When .95 below to .05 above a penny, use that penny.
Returns
-------
float
In math-speak:
If prefer_round_down: [<X-1>.0095, X.0195) -> round to X.01.
If not prefer_round_down: (<X-1>.0005, X.0105] -> round to X.01.
""" """
# Subtracting an epsilon from diff to enforce the open-ness of the upper # TODO: consider overriding outside of the original function
# bound on buys and the lower bound on sells. Using the actual system return price
# epsilon doesn't quite get there, so use a slightly less epsilon-ey value.
epsilon = float_info.epsilon * 10
diff = diff - epsilon
# relies on rounding half away from zero, unlike numpy's bankers' rounding
rounded = round(price - (diff if prefer_round_down else -diff), 2)
if zp_math.tolerant_equals(rounded, 0.0):
return 0.0
return rounded
def check_stoplimit_prices(price, label): def check_stoplimit_prices(price, label):
+27 -14
View File
@@ -22,7 +22,7 @@ from pandas.tseries.tools import normalize_date
from six import iteritems from six import iteritems
from . risk import ( from .risk import (
check_entry, check_entry,
choose_treasury choose_treasury
) )
@@ -37,12 +37,11 @@ from empyrical import (
sharpe_ratio, sharpe_ratio,
sortino_ratio, sortino_ratio,
) )
import warnings
from catalyst.constants import LOG_LEVEL from catalyst.constants import LOG_LEVEL
log = logbook.Logger('Risk Cumulative', level=LOG_LEVEL) log = logbook.Logger('Risk Cumulative', level=LOG_LEVEL)
choose_treasury = functools.partial(choose_treasury, lambda *args: '10year', choose_treasury = functools.partial(choose_treasury, lambda *args: '10year',
compound=False) compound=False)
@@ -145,6 +144,8 @@ class RiskMetricsCumulative(object):
self.num_trading_days = 0 self.num_trading_days = 0
def update(self, dt, algorithm_returns, benchmark_returns, leverage): def update(self, dt, algorithm_returns, benchmark_returns, leverage):
warnings.filterwarnings('error')
# Keep track of latest dt for use in to_dict and other methods # Keep track of latest dt for use in to_dict and other methods
# that report current state. # that report current state.
self.latest_dt = dt self.latest_dt = dt
@@ -191,9 +192,12 @@ class RiskMetricsCumulative(object):
if len(self.benchmark_returns) == 1: if len(self.benchmark_returns) == 1:
self.benchmark_returns = np.append(0.0, self.benchmark_returns) self.benchmark_returns = np.append(0.0, self.benchmark_returns)
self.benchmark_cumulative_returns[dt_loc] = cum_returns( try:
self.benchmark_returns self.benchmark_cumulative_returns[dt_loc] = cum_returns(
)[-1] self.benchmark_returns
)[-1]
except Exception:
self.benchmark_cumulative_returns[dt_loc] = 0
benchmark_cumulative_returns_to_date = \ benchmark_cumulative_returns_to_date = \
self.benchmark_cumulative_returns[:dt_loc + 1] self.benchmark_cumulative_returns[:dt_loc + 1]
@@ -268,10 +272,17 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
self.downside_risk[dt_loc] = downside_risk( self.downside_risk[dt_loc] = downside_risk(
self.algorithm_returns self.algorithm_returns
) )
self.sortino[dt_loc] = sortino_ratio(
self.algorithm_returns, try:
_downside_risk=self.downside_risk[dt_loc] risk = self.downside_risk[dt_loc]
) self.sortino[dt_loc] = sortino_ratio(
self.algorithm_returns,
_downside_risk=risk
)
except Exception:
# TODO: what causes it to error out?
self.sortino[dt_loc] = 0
self.information[dt_loc] = information_ratio( self.information[dt_loc] = information_ratio(
self.algorithm_returns, self.algorithm_returns,
self.benchmark_returns, self.benchmark_returns,
@@ -283,6 +294,8 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
self.max_leverage = self.calculate_max_leverage() self.max_leverage = self.calculate_max_leverage()
self.max_leverages[dt_loc] = self.max_leverage self.max_leverages[dt_loc] = self.max_leverage
warnings.resetwarnings()
def to_dict(self): def to_dict(self):
""" """
Creates a dictionary representing the state of the risk report. Creates a dictionary representing the state of the risk report.
@@ -294,18 +307,18 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
rval = { rval = {
'trading_days': self.num_trading_days, 'trading_days': self.num_trading_days,
'benchmark_volatility': 'benchmark_volatility':
self.benchmark_volatility[dt_loc], self.benchmark_volatility[dt_loc],
'algo_volatility': 'algo_volatility':
self.algorithm_volatility[dt_loc], self.algorithm_volatility[dt_loc],
'treasury_period_return': self.treasury_period_return, 'treasury_period_return': self.treasury_period_return,
# Though the two following keys say period return, # Though the two following keys say period return,
# they would be more accurately called the cumulative return. # they would be more accurately called the cumulative return.
# However, the keys need to stay the same, for now, for backwards # However, the keys need to stay the same, for now, for backwards
# compatibility with existing consumers. # compatibility with existing consumers.
'algorithm_period_return': 'algorithm_period_return':
self.algorithm_cumulative_returns[dt_loc], self.algorithm_cumulative_returns[dt_loc],
'benchmark_period_return': 'benchmark_period_return':
self.benchmark_cumulative_returns[dt_loc], self.benchmark_cumulative_returns[dt_loc],
'beta': self.beta[dt_loc], 'beta': self.beta[dt_loc],
'alpha': self.alpha[dt_loc], 'alpha': self.alpha[dt_loc],
'sharpe': self.sharpe[dt_loc], 'sharpe': self.sharpe[dt_loc],
+25 -9
View File
@@ -14,6 +14,7 @@
# limitations under the License. # limitations under the License.
import functools import functools
import warnings
import logbook import logbook
@@ -23,7 +24,7 @@ import numpy as np
import pandas as pd import pandas as pd
from . import risk from . import risk
from . risk import check_entry from .risk import check_entry
from empyrical import ( from empyrical import (
alpha_beta_aligned, alpha_beta_aligned,
@@ -78,14 +79,20 @@ class RiskMetricsPeriod(object):
self.calculate_metrics() self.calculate_metrics()
def calculate_metrics(self): def calculate_metrics(self):
self.benchmark_period_returns = \ warnings.filterwarnings('error')
cum_returns(self.benchmark_returns).iloc[-1]
try:
self.benchmark_period_returns = \
cum_returns(self.benchmark_returns).iloc[-1]
except Exception:
# TODO: why is there an error
self.benchmark_period_returns = 0
self.algorithm_period_returns = \ self.algorithm_period_returns = \
cum_returns(self.algorithm_returns).iloc[-1] cum_returns(self.algorithm_returns).iloc[-1]
if not self.algorithm_returns.index.equals( if not self.algorithm_returns.index.equals(
self.benchmark_returns.index self.benchmark_returns.index
): ):
message = "Mismatch between benchmark_returns ({bm_count}) and \ message = "Mismatch between benchmark_returns ({bm_count}) and \
algorithm_returns ({algo_count}) in range {start} : {end}" algorithm_returns ({algo_count}) in range {start} : {end}"
@@ -128,10 +135,17 @@ class RiskMetricsPeriod(object):
self.downside_risk = downside_risk( self.downside_risk = downside_risk(
self.algorithm_returns.values self.algorithm_returns.values
) )
self.sortino = sortino_ratio(
self.algorithm_returns.values, try:
_downside_risk=self.downside_risk, risk = self.downside_risk
) self.sortino = sortino_ratio(
self.algorithm_returns.values,
_downside_risk=risk,
)
except Exception:
# TODO: what causes it to error out?
self.sortino = 0
self.information = information_ratio( self.information = information_ratio(
self.algorithm_returns.values, self.algorithm_returns.values,
self.benchmark_returns.values, self.benchmark_returns.values,
@@ -141,10 +155,12 @@ class RiskMetricsPeriod(object):
self.benchmark_returns.values, self.benchmark_returns.values,
) )
self.excess_return = self.algorithm_period_returns - \ self.excess_return = self.algorithm_period_returns - \
self.treasury_period_return self.treasury_period_return
self.max_drawdown = max_drawdown(self.algorithm_returns.values) self.max_drawdown = max_drawdown(self.algorithm_returns.values)
self.max_leverage = self.calculate_max_leverage() self.max_leverage = self.calculate_max_leverage()
warnings.resetwarnings()
def to_dict(self): def to_dict(self):
""" """
Creates a dictionary representing the state of the risk report. Creates a dictionary representing the state of the risk report.
+13 -14
View File
@@ -1,6 +1,6 @@
""" """
Requires Catalyst version 0.3.0 or above Requires Catalyst version 0.3.0 or above
Tested on Catalyst version 0.3.2 Tested on Catalyst version 0.3.3
These example aims to provide and easy way for users to learn how to collect data from the different exchanges. These example aims to provide and easy way for users to learn how to collect data from the different exchanges.
You simply need to specify the exchange and the market that you want to focus on. You simply need to specify the exchange and the market that you want to focus on.
@@ -27,7 +27,7 @@ from catalyst.api import (
def initialize(context): def initialize(context):
context.i = -1 # counts the minutes context.i = -1 # counts the minutes
context.exchange = 'poloniex' # must match the exchange specified in run_algorithm context.exchange = 'poloniex' # must match the exchange specified in run_algorithm
context.base_currency = 'eth' # must match the base currency specified in run_algorithm context.base_currency = 'btc' # must match the base currency specified in run_algorithm
def handle_data(context, data): def handle_data(context, data):
@@ -56,21 +56,21 @@ def handle_data(context, data):
# 30 minute interval ohlcv data (the standard data required for candlestick or indicators/signals) # 30 minute interval ohlcv data (the standard data required for candlestick or indicators/signals)
# 30T means 30 minutes re-sampling of one minute data. change to your desire time interval. # 30T means 30 minutes re-sampling of one minute data. change to your desire time interval.
open = fill(data.history(coin, 'open', bar_count=lookback, opened = fill(data.history(coin, 'open', bar_count=lookback,
frequency='1m')).resample('30T').first() frequency='30T')).values
high = fill(data.history(coin, 'high', bar_count=lookback, high = fill(data.history(coin, 'high', bar_count=lookback,
frequency='1m')).resample('30T').max() frequency='30T')).values
low = fill(data.history(coin, 'low', bar_count=lookback, low = fill(data.history(coin, 'low', bar_count=lookback,
frequency='1m')).resample('30T').min() frequency='30T')).values
close = fill(data.history(coin, 'price', bar_count=lookback, close = fill(data.history(coin, 'price', bar_count=lookback,
frequency='1m')).resample('30T').last() frequency='30T')).values
volume = fill(data.history(coin, 'volume', bar_count=lookback, volume = fill(data.history(coin, 'volume', bar_count=lookback,
frequency='1m')).resample('30T').sum() frequency='30T')).values
# close[-1] is the equivalent to current price # close[-1] is the equivalent to current price
# displays the minute price for each pair every 30 minutes # displays the minute price for each pair every 30 minutes
print( print(
today, pair, open[-1], high[-1], low[-1], close[-1], volume[-1]) today, pair, opened[-1], high[-1], low[-1], close[-1], volume[-1])
# ---------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------
# -------------------------------------- Insert Your Strategy Here ----------------------------------------- # -------------------------------------- Insert Your Strategy Here -----------------------------------------
@@ -82,7 +82,7 @@ def analyze(context=None, results=None):
# Get the universe for a given exchange and a given base_currency market # Get the universe for a given exchange and a given base_currency market
# Example: Poloniex BTC Market # Example: Poloniex btc Market
def universe(context, lookback_date, current_date): def universe(context, lookback_date, current_date):
json_symbols = get_exchange_symbols( json_symbols = get_exchange_symbols(
context.exchange) # get all the pairs for the exchange context.exchange) # get all the pairs for the exchange
@@ -103,7 +103,6 @@ def universe(context, lookback_date, current_date):
universe_df = universe_df[universe_df.end_daily >= current_date] universe_df = universe_df[universe_df.end_daily >= current_date]
context.coins = symbols( context.coins = symbols(
*universe_df.symbol) # convert all the pairs to symbols *universe_df.symbol) # convert all the pairs to symbols
print(universe_df.head(), len(universe_df))
return universe_df.symbol.tolist() return universe_df.symbol.tolist()
@@ -119,8 +118,8 @@ def fill(series):
if __name__ == '__main__': if __name__ == '__main__':
start_date = pd.to_datetime('2017-01-01', utc=True) start_date = pd.to_datetime('2017-01-08', utc=True)
end_date = pd.to_datetime('2017-10-15', utc=True) end_date = pd.to_datetime('2017-11-13', utc=True)
performance = run_algorithm(start=start_date, end=end_date, performance = run_algorithm(start=start_date, end=end_date,
capital_base=10000.0, capital_base=10000.0,
@@ -129,7 +128,7 @@ if __name__ == '__main__':
analyze=analyze, analyze=analyze,
exchange_name='poloniex', exchange_name='poloniex',
data_frequency='minute', data_frequency='minute',
base_currency='eth', base_currency='btc',
live=False, live=False,
live_graph=False, live_graph=False,
algo_namespace='simple_universe') algo_namespace='simple_universe')
+42
View File
@@ -0,0 +1,42 @@
import talib
import pandas as pd
from catalyst import run_algorithm
from catalyst.api import symbol
def initialize(context):
print('initializing')
context.asset = symbol('xcp_btc')
def handle_data(context, data):
print('handling bar: {}'.format(data.current_dt))
price = data.current(context.asset, 'close')
print('got price {price}'.format(price=price))
try:
prices = data.history(
context.asset,
fields='close',
bar_count=1,
frequency='1D'
)
print('got {} price entries\n'.format(len(prices), prices))
except Exception as e:
print(e)
run_algorithm(
capital_base=1,
start=pd.to_datetime('2015-3-2', utc=True),
end=pd.to_datetime('2017-8-31', utc=True),
data_frequency='daily',
initialize=initialize,
handle_data=handle_data,
analyze=None,
exchange_name='poloniex',
algo_namespace='issue_55',
base_currency='btc'
)
+46
View File
@@ -0,0 +1,46 @@
import talib
import pandas as pd
from catalyst import run_algorithm
from catalyst.api import symbol
def initialize(context):
print('initializing')
context.asset = symbol('btc_usdt')
def handle_data(context, data):
print('handling bar: {}'.format(data.current_dt))
price = data.current(context.asset, 'close')
print('got price {price}'.format(price=price))
try:
prices = data.history(
context.asset,
fields='close',
bar_count=60,
frequency='1D'
)
print('got {} price entries\n'.format(len(prices), prices))
except Exception as e:
print(e)
run_algorithm(
capital_base=1,
start=pd.to_datetime('2016-2-11', utc=True),
end=pd.to_datetime('2017-8-31', utc=True),
data_frequency='daily',
initialize=initialize,
handle_data=handle_data,
analyze=None,
exchange_name='bittrex',
algo_namespace='issue_57',
base_currency='btc'
<<<<<<< HEAD
)
=======
)
>>>>>>> develop
+127
View File
@@ -0,0 +1,127 @@
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='daily')
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, )
+67 -11
View File
@@ -1,4 +1,5 @@
import os import os
import re
import sys import sys
import warnings import warnings
from datetime import timedelta from datetime import timedelta
@@ -8,6 +9,8 @@ from time import sleep
import click import click
import pandas as pd import pandas as pd
from catalyst.data.bundles import load
from catalyst.data.data_portal import DataPortal
from catalyst.exchange.bittrex.bittrex import Bittrex from catalyst.exchange.bittrex.bittrex import Bittrex
from catalyst.exchange.bitfinex.bitfinex import Bitfinex from catalyst.exchange.bitfinex.bitfinex import Bitfinex
from catalyst.exchange.poloniex.poloniex import Poloniex from catalyst.exchange.poloniex.poloniex import Poloniex
@@ -167,10 +170,12 @@ def _run(handle_data,
# This corresponds to the json file containing api token info # This corresponds to the json file containing api token info
exchange_auth = get_exchange_auth(exchange_name) exchange_auth = get_exchange_auth(exchange_name)
if live and (exchange_auth['key'] == '' or exchange_auth['secret'] == ''): if live and (exchange_auth['key'] == '' \
or exchange_auth['secret'] == ''):
raise ExchangeAuthEmpty( raise ExchangeAuthEmpty(
exchange=exchange_name.title(), exchange=exchange_name.title(),
filename=os.path.join(get_exchange_folder(exchange_name, environ), 'auth.json') ) filename=os.path.join(
get_exchange_folder(exchange_name, environ), 'auth.json'))
if exchange_name == 'bitfinex': if exchange_name == 'bitfinex':
exchanges[exchange_name] = Bitfinex( exchanges[exchange_name] = Bitfinex(
@@ -258,17 +263,35 @@ def _run(handle_data,
) )
if base_currency in balances: if base_currency in balances:
return balances[base_currency] base_currency_available = balances[base_currency]
log.info(
'base currency available in the account: {} {}'.format(
base_currency_available, base_currency
)
)
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
else: else:
raise BaseCurrencyNotFoundError( raise BaseCurrencyNotFoundError(
base_currency=base_currency, base_currency=base_currency,
exchange=exchange_name exchange=exchange_name
) )
capital_base = 0 combined_capital_base = 0
for exchange_name in exchanges: for exchange_name in exchanges:
exchange = exchanges[exchange_name] exchange = exchanges[exchange_name]
capital_base += fetch_capital_base(exchange) combined_capital_base += fetch_capital_base(exchange)
sim_params = create_simulation_parameters( sim_params = create_simulation_parameters(
start=start, start=start,
@@ -287,7 +310,7 @@ def _run(handle_data,
algo_namespace=algo_namespace, algo_namespace=algo_namespace,
live_graph=live_graph live_graph=live_graph
) )
else: elif exchanges:
# Removed the existing Poloniex fork to keep things simple # Removed the existing Poloniex fork to keep things simple
# We can add back the complexity if required. # We can add back the complexity if required.
@@ -297,7 +320,7 @@ def _run(handle_data,
# can handle this later. # can handle this later.
data = DataPortalExchangeBacktest( data = DataPortalExchangeBacktest(
exchanges=exchanges, exchange_names=[exchange_name for exchange_name in exchanges],
asset_finder=None, asset_finder=None,
trading_calendar=open_calendar, trading_calendar=open_calendar,
first_trading_day=start, first_trading_day=start,
@@ -317,6 +340,36 @@ def _run(handle_data,
exchanges=exchanges 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( perf = algorithm_class(
namespace=namespace, namespace=namespace,
env=env, env=env,
@@ -416,7 +469,8 @@ def run_algorithm(initialize,
exchange_name=None, exchange_name=None,
base_currency=None, base_currency=None,
algo_namespace=None, algo_namespace=None,
live_graph=False): live_graph=False,
output=os.devnull):
"""Run a trading algorithm. """Run a trading algorithm.
Parameters Parameters
@@ -486,7 +540,9 @@ def run_algorithm(initialize,
-------- --------
catalyst.data.bundles.bundles : The available data bundles. catalyst.data.bundles.bundles : The available data bundles.
""" """
load_extensions(default_extension, extensions, strict_extensions, environ) load_extensions(
default_extension, extensions, strict_extensions, environ
)
# I'm not sure that we need this since the modified DataPortal # I'm not sure that we need this since the modified DataPortal
# does not require extensions to be explicitly loaded. # does not require extensions to be explicitly loaded.
@@ -527,7 +583,7 @@ def run_algorithm(initialize,
bundle_timestamp=bundle_timestamp, bundle_timestamp=bundle_timestamp,
start=start, start=start,
end=end, end=end,
output=os.devnull, output=output,
print_algo=False, print_algo=False,
local_namespace=False, local_namespace=False,
environ=environ, environ=environ,
+166 -84
View File
@@ -5,9 +5,8 @@ Basics
~~~~~~ ~~~~~~
Catalyst is an open-source algorithmic trading simulator for crypto Catalyst is an open-source algorithmic trading simulator for crypto
assets written in Python. assets written in Python. The source code can be found at:
https://github.com/enigmampc/catalyst
The source can be found at: https://github.com/enigmampc/catalyst
Some benefits include: Some benefits include:
@@ -25,8 +24,7 @@ Some benefits include:
build profitable, data-driven investment strategies. build profitable, data-driven investment strategies.
This tutorial assumes that you have Catalyst correctly installed, see the This tutorial assumes that you have Catalyst correctly installed, see the
:doc:`installation instructions <install>` if you haven't set up :doc:`Install<install>` section if you haven't set up Catalyst yet.
Catalyst yet.
Every ``catalyst`` algorithm consists of at least two functions you have to Every ``catalyst`` algorithm consists of at least two functions you have to
define: define:
@@ -40,10 +38,12 @@ Before the start of the algorithm, ``catalyst`` calls the
need to access from one algorithm iteration to the next. need to access from one algorithm iteration to the next.
After the algorithm has been initialized, ``catalyst`` calls the After the algorithm has been initialized, ``catalyst`` calls the
``handle_data()`` function once for each event. At every call, it passes ``handle_data()`` function on each iteration, that's one per day (daily) or
the same ``context`` variable and an event-frame called ``data`` once every minute (minute), depending on the frequency we choose to run our
containing the current trading bar with open, high, low, and close simulation. On every iteration, ``handle_data()`` passes the same ``context``
(OHLC) prices as well as volume for each crypto asset in your universe. variable and an event-frame called ``data`` containing the current trading bar
with open, high, low, and close (OHLC) prices as well as volume for each
crypto asset in your universe.
.. For more information on these functions, see the `relevant part of the .. For more information on these functions, see the `relevant part of the
.. Quantopian docs <https://www.quantopian.com/help#api-toplevel>`. .. Quantopian docs <https://www.quantopian.com/help#api-toplevel>`.
@@ -51,8 +51,8 @@ containing the current trading bar with open, high, low, and close
My first algorithm My first algorithm
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
Lets take a look at a very simple algorithm from the ``examples`` Lets take a look at a very simple algorithm from the ``examples`` directory:
directory: `buy_btc_simple.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_btc_simple.py>`_: `buy_btc_simple.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_btc_simple.py>`_:
.. code-block:: python .. code-block:: python
@@ -70,9 +70,9 @@ directory: `buy_btc_simple.py <https://github.com/enigmampc/catalyst/blob/master
As you can see, we first have to import some functions we would like to As you can see, we first have to import some functions we would like to
use. All functions commonly used in your algorithm can be found in use. All functions commonly used in your algorithm can be found in
``catalyst.api``. Here we are using :func:`~catalyst.api.order()` which takes two ``catalyst.api``. Here we are using :func:`~catalyst.api.order()` which takes
arguments: a cryptoasset object, and a number specifying how many assets you would twoarguments: a cryptoasset object, and a number specifying how many assets you
like to order (if negative, :func:`~catalyst.api.order()` will sell/short wouldlike to order (if negative, :func:`~catalyst.api.order()` will sell/short
assets). In this case we want to order 1 bitcoin at each iteration. assets). In this case we want to order 1 bitcoin at each iteration.
.. For more documentation on ``order()``, see the `Quantopian docs .. For more documentation on ``order()``, see the `Quantopian docs
@@ -88,61 +88,98 @@ a bitcoin in the ``data`` event frame.
.. (for more information see `here <https://www.quantopian.com/help#api-event-properties>`__. .. (for more information see `here <https://www.quantopian.com/help#api-event-properties>`__.
Running the algorithm
~~~~~~~~~~~~~~~~~~~~~
To can now test this algorithm on crypto data, ``catalyst`` provides three
interfaces:
- A command-line interface,
- ``IPython Notebook`` magic,
- and :func:`~catalyst.run_algorithm`.
Ingesting data Ingesting data
^^^^^^^^^^^^^^ ~~~~~~~~~~~~~~
In previous versions of Catalyst you needed to manually ingest data before running Before you can backtest your algorithm, you first need to load the historical
your algorithm to make it available at runtime. Starting with version 0.3, the pricing data that Catalyst needs to run your simulation through a process called
algorithm will automagically ingest the data it needs the first time that encounters ``ingestion``. When you ingest data, Catalyst downloads that data in compressed
a data request for data that it doesn't have. form from the Enigma servers (which eventually will migrate to the Enigma Data
Marketplace), and stores it locally to make it available at runtime.
Still, we believe it is important for you to have a high-level understanding In order to ingest data, you need to run a command like the following:
of how data is managed:
.. code-block:: bash
catalyst ingest-exchange -x bitfinex -i btc_usd
This instructs Catalyst to download pricing data from the ``Bitfinex`` exchange
for the ``btc_usd`` currency pair (this follows from the simple algorithm
presented above where we want to trade ``btc_usd``), and we're choosing to test
our algorithm using historical pricing data from the Bitfinex exchange. By
default, Catalyst assumes that you want data with ``daily`` frequency (one candle
bar per day). If you want instead ``minute`` frequency (one candle bar for every
minute), you would need to specify it as follows:
.. code-block:: bash
catalyst ingest-exchange -x bitfinex -i btc_usd -f minute
.. parsed-literal::
Ingesting exchange bundle bitfinex...
[====================================] Ingesting daily price data on bitfinex: 100%
We believe it is important for you to have a high-level understanding of how
data is managed, hence the following overview:
- Pricing data is split and packaged into ``bundles``: chunks of data organized - Pricing data is split and packaged into ``bundles``: chunks of data organized
as time series that are kept up to date daily on Enigma's servers. Catalyst as time series that are kept up to date daily on Enigma's servers. Catalyst
downloads the bundles that needs at any given time, and reconstructs the whole downloads the requested bundles and reconstructs the full dataset in your
dataset in your hard drive. hard drive.
- Pricing data is provided in ``daily`` and ``minute`` resolution. Those are different - Pricing data is provided in ``daily`` and ``minute`` resolution. Those are
bundle datasets, and are managed separately. different bundle datasets, and are managed separately.
- Bundles are exchange-specific, as the pricing data is specific to the trades that - Bundles are exchange-specific, as the pricing data is specific to the trades
happen in each exchange. You can optionally specify which exchange you want pricing that happen in each exchange. As a result, you can must specify which
data from. exchange you want pricing data from when ingesting data
- Catalyst keeps track of all the downloaded bundles, so that it only has to download - Catalyst keeps track of all the downloaded bundles, so that it only has to
them once, and will do incremental updates as needed. download them once, and will do incremental updates as needed.
- When running in ``live trading`` mode, Catalyst will first look for historical - When running in ``live trading`` mode, Catalyst will first look for
pricing data in the locally stored bundles. If there is anything missing, Catalyst will historical pricing data in the locally stored bundles. If there is anything
hit the exchange for the most recent data, and merge it with the local bundle to make missing, Catalyst will hit the exchange for the most recent data, and merge
it available for future iterations. it with the local bundle to optimize the number of requests it needs to make
to the exchange.
If you want to learn more, check out the :ref:`ingesting data <ingesting-data>` section The ``ingest-exchange`` command in catalyst offers additional parameters to
for more detail. further tweak the data ingestion process. You can learn more by running the
following from the command line:
.. code-block:: bash
catalyst ingest-exchange --help
Running the algorithm
~~~~~~~~~~~~~~~~~~~~~
You can now test your algorithm using cryptoassets' historical pricing data,
``catalyst`` provides three interfaces:
- A command-line interface (CLI),
- the ``IPython Notebook`` magic,
- and a :func:`~catalyst.run_algorithm` that you can call from other
Python scripts.
We'll start with the CLI, and introduce the ``IPython Notebook`` below. Some of
the :doc:`example algorithms <example-algos>` provide instructions on how to run
them both from the CLI, and using the :func:`~catalyst.run_algorithm` function.
Command line interface Command line interface
^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
After you installed Catalyst you should be able to execute the following After you installed Catalyst, you should be able to execute the following
from your command line (e.g. ``cmd.exe`` on Windows, or the Terminal app from your command line (e.g. ``cmd.exe`` or the ``Anaconda Prompt`` on Windows,
on OSX). Displaying here a simplified output for eductional purposes: or the Terminal application on MacOS).
.. code-block:: bash .. code-block:: bash
$ catalyst --help $ catalyst --help
This is the resulting output, simplified for eductional purposes:
.. parsed-literal:: .. parsed-literal::
Usage: catalyst [OPTIONS] COMMAND [ARGS]... Usage: catalyst [OPTIONS] COMMAND [ARGS]...
@@ -158,10 +195,11 @@ on OSX). Displaying here a simplified output for eductional purposes:
live Trade live with the given algorithm. live Trade live with the given algorithm.
run Run a backtest for the given algorithm. run Run a backtest for the given algorithm.
There are three main modes you can run on Catalyst. The first being ``ingest-exchange`` There are three main modes you can run on Catalyst. The first being
for data ingestion, which we have summarized in the previous section. The second ``ingest-exchange`` for data ingestion, which we have covered in the previous
is ``live`` to use your algorithm to trade live against a given exchange, and the section. The second is ``live`` to use your algorithm to trade live against a
third mode ``run`` is to backtest your algorithm before trading live with it. given exchange, and the third mode ``run`` is to backtest your algorithm before
trading live with it.
Let's start with backtesting, so run this other command to learn more about Let's start with backtesting, so run this other command to learn more about
the available options: the available options:
@@ -210,22 +248,24 @@ the available options:
As you can see there are a couple of flags that specify where to find your As you can see there are a couple of flags that specify where to find your
algorithm (``-f``) as well as a parameter to specify which exchange to use. algorithm (``-f``) as well as a the ``-x`` flag to specify which exchange to
There are also arguments for the date range to run the algorithm over use. There are also arguments for the date range to run the algorithm over
(``--start`` and ``--end``). Finally, you'll want to save the performance (``--start`` and ``--end``). You also need to set the base currency for your
metrics of your algorithm so that you can analyze how it performed. This is algorithm through the ``-c`` flag, and the ``--capital_base``. All the
done via the ``--output`` flag and will cause it to write the performance aforementioned parameters are required. Optionally, you will want to save the
``DataFrame`` in the pickle Python file format. Note that you can also define performance metrics of your algorithm so that you can analyze how it performed.
a configuration file with these parameters that you can then conveniently pass This is done via the ``--output`` flag and will cause it to write the
to the ``-c`` option so that you don't have to supply the command line args performance ``DataFrame`` in the pickle Python file format. Note that you can
all the time (see the .conf files in the examples directory). also define a configuration file with these parameters that you can then
conveniently pass to the ``-c`` option so that you don't have to supply the
command line args all the time.
Thus, to execute our algorithm from above and save the results to Thus, to execute our algorithm from above and save the results to
``buy_btc_simple_out.pickle`` we would call ``catalyst run`` as follows: ``buy_btc_simple_out.pickle`` we would call ``catalyst run`` as follows:
.. code-block:: python .. code-block:: python
catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 --end 2017-9-30 -c usd --capital-base 100000 -o buy_btc_simple_out.pickle
.. parsed-literal:: .. parsed-literal::
@@ -253,17 +293,25 @@ slippage model that ``catalyst`` uses).
.. see the `Quantopian docs <https://www.quantopian.com/help#ide-slippage>`__ .. see the `Quantopian docs <https://www.quantopian.com/help#ide-slippage>`__
.. for more information). .. for more information).
Let's take a quick look at the performance ``DataFrame``. For this, we
use ``pandas`` from inside the IPython Notebook and print the first ten Let's take a quick look at the performance ``DataFrame``. For this, we write
rows. Note that ``catalyst`` makes heavy usage of different Python script--let's call it ``print_results.py``--and we make use of
`pandas <http://pandas.pydata.org/>`_, especially for data input and the fantastic ``pandas`` library to print the first ten rows. Note that
outputting so it's worth spending some time to learn it. ``catalyst`` makes heavy usage of `pandas <http://pandas.pydata.org/>`_,
especially for data analysis and outputting so it's worth spending some time to
learn it.
.. code-block:: python .. code-block:: python
import pandas as pd import pandas as pd
perf = pd.read_pickle('buy_btc_simple_out.pickle') # read in perf DataFrame perf = pd.read_pickle('buy_btc_simple_out.pickle') # read in perf DataFrame
perf.head() print(perf.head())
Which we execute by running:
.. code-block:: bash
$ python print_results.py
.. raw:: html .. raw:: html
@@ -429,30 +477,48 @@ and allows us to plot the price of bitcoin. For example, we could easily
examine now how our portfolio value changed over time compared to the examine now how our portfolio value changed over time compared to the
bitcoin price. bitcoin price.
.. code-block:: python Now we will run the simulation again, but this time we extend our original
algorithm with the addition of the ``analyze()`` function. Somewhat analogously
%load_ext catalyst as how ``initialize()`` gets called once before the start of the algorith,
``analyze()`` gets called once at the end of the algorithm, and receives two
variables: ``context``, which we discussed at the very beginning, and ``perf``,
which is the pandas dataframe containing the performance data for our algorithm
that we reviewed above. Inside the ``analyze()`` function is where we can
analyze and visualize the results of our strategy. Here's the revised simple
algorithm (note the addition of Line 1, and Lines 11-18)
.. code-block:: python .. code-block:: python
%pylab inline
figsize(12, 12)
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from catalyst.api import order, record, symbol
ax1 = plt.subplot(211) def initialize(context):
perf.portfolio_value.plot(ax=ax1) context.asset = symbol('btc_usd')
ax1.set_ylabel('portfolio value')
ax2 = plt.subplot(212, sharex=ax1)
perf.btc.plot(ax=ax2)
ax2.set_ylabel('bitcoin price')
.. parsed-literal:: def handle_data(context, data):
order(context.asset, 1)
record(btc = data.current(context.asset, 'price'))
Populating the interactive namespace from numpy and matplotlib def analyze(context, perf):
ax1 = plt.subplot(211)
perf.portfolio_value.plot(ax=ax1)
ax1.set_ylabel('portfolio value')
ax2 = plt.subplot(212, sharex=ax1)
perf.btc.plot(ax=ax2)
ax2.set_ylabel('bitcoin price')
plt.show()
.. parsed-literal:: Here we make use of the external visualization library called
`matplotlib <https://matplotlib.org/>`_, which you might recall we installed
alongside enigma-catalyst (with the exception of the ``Conda`` install, where it
was included by default inside the conda environment we created). If for any
reason you don't have it installed, you can add it by running:
<matplotlib.text.Text at 0x10eaeadd0> .. code-block:: python
(catalyst)$ pip install matplotlib
If everything works well, you'll see the following chart:
.. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/buy_btc_simple_graph.png .. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/buy_btc_simple_graph.png
@@ -460,6 +526,22 @@ Our algorithm performance as assessed by the ``portfolio_value`` closely
matches that of the bitcoin price. This is not surprising as our algorithm matches that of the bitcoin price. This is not surprising as our algorithm
only bought bitcoin every chance it got. only bought bitcoin every chance it got.
If you get an error when invoking matplotlib to visualize the performance
results refer to `MacOS + Matplotlib <install.html#macos-virtualenv-matplotlib>`_.
Alternatively, some users have reported the following error when running an algo
in a Linux environment:
.. parsed-literal::
ImportError: No module named _tkinter, please install the python-tk package
Which can easily solved by running (in Ubuntu/Debian-based systems):
.. code-block:: python
sudo apt install python-tk
Access to previous prices using ``history`` Access to previous prices using ``history``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+444
View File
@@ -0,0 +1,444 @@
|
Example Algorithms
==================
This section documents a small number of example algorithms to complement the
beginner tutorial, and show how other trading algorithms can be implemented
using Catalyst:
.. _buy_and_hodl:
Buy and Hodl Algorithm
~~~~~~~~~~~~~~~~~~~~~~
source: `examples/buy_and_hodl.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/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
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
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
date interval with the ``--start`` and ``--end`` parameters, but bear in mind
that 2015-3-1 is the earliest date that Catalyst supports (if you choose an
earlier date, you'll get an error), and the most recent date you can choose is
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
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
context.is_buying = True
context.asset = symbol(context.ASSET_NAME)
context.i = 0
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
# 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
# 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,
)
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
# 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)
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',
)
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)')
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')
ax6 = plt.subplot(616, sharex=ax1)
results[['volume']].plot(ax=ax6)
ax6.set_ylabel('Volume (mCoins/5min)')
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
.. _mean_reversion:
Mean Reversion Algorithm
~~~~~~~~~~~~~~~~~~~~~~~~
source: `examples/mean_reversion_simple.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/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
that we need, with minute resolution:
.. code-block:: bash
catalyst ingest-exchange -x bitfinex -f minute -i neo_usd
To run this algorithm, we are opting for the Python interpreter, instead of the
command line (CLI). All of the parameters for the simulation are specified in
lines 218-245, so in order to run the algorithm we just type:
.. code-block:: bash
python mean_reversion_simple.py
.. code-block:: python
import pandas as pd
import talib
from logbook import Logger
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.
NAMESPACE = 'mean_reversion_simple'
log = Logger(NAMESPACE)
# 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
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.
# 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
# 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.
# 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'
)
# 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
def analyze(context=None, perf=None):
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 Value ({})'.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(
asset=context.neo_usd.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, '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=''
)
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))
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')
ax6 = plt.subplot(615, sharex=ax1)
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
ax6.axhline(70, color='darkgoldenrod')
ax6.axhline(30, color='darkgoldenrod')
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)
# 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':
# 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
)
+2
View File
@@ -12,6 +12,8 @@ Table of Contents
jupyter jupyter
live-trading live-trading
naming-convention naming-convention
example-algos
utilities
videos videos
resources resources
development-guidelines development-guidelines
+271 -213
View File
@@ -6,7 +6,154 @@ Like any other piece of software, Catalyst has a number of dependencies
(other software on which it depends to run) that you will need to install, as (other software on which it depends to run) that you will need to install, as
well. We recommend using a software named ``Conda`` that will manage all well. We recommend using a software named ``Conda`` that will manage all
these dependencies for you, and set up the environment needed to get you up these dependencies for you, and set up the environment needed to get you up
and running as easily as possible. See :ref:`Installing with Conda <conda>`. and running as easily as possible. This is the recommended installation method
for Windows, MacOS and Linux. See :ref:`Installing with Conda <conda>`.
What conda does is create a pre-configured environment, and inside that
environment install Catalyst using ``pip``, Python's package manager. Thus,
as an alternative installation method for MacOS and Linux, you can install
Catalyst directly with ``pip`` (we recommend in combination with a virtual
environemnt). See :ref:`Installing with pip <pip>`.
Regardless of the method, each operating system (OS), has its own
prerequisites, make sure to review the corresponding sections for your system:
:ref:`Linux <linux>`, :ref:`MacOS <macos>` and :ref:`Windows <windows>`.
.. _conda:
Installing with ``conda``
-------------------------
The preferred method to install Catalyst is via the ``conda`` package manager,
which comes as part of Continuum Analytics' `Anaconda
<http://continuum.io/downloads>`_ distribution.
The primary advantage of using Conda over ``pip`` is that conda natively
understands the complex binary dependencies of packages like ``numpy`` and
``scipy``. This means that ``conda`` can install Catalyst and its
dependencies without requiring the use of a second tool to acquire Catalyst's
non-Python dependencies.
For Windows, you will first need to install the *Microsoft Visual C++
Compiler for Python 2.7*. Follow the instructions on the :ref:`Windows
<windows>` section and come back here.
For instructions on how to install ``conda``, see the `Conda Installation
Documentation <http://conda.pydata.org/docs/download.html>`_. Alternatively,
you can install MiniConda, which is a smaller footprint (fewer packages and
smaller size) than its big brother Anaconda, but it still contains all the
main packages needed. To install MiniConda, you can follow these steps:
1. Download `MiniConda <https://conda.io/miniconda.html>`_. Select Python 2.7
for your Operating System.
2. Install MiniConda. See the `Installation Instructions
<https://conda.io/docs/user-guide/install/index.html>`_ if you need help.
3. Ensure the correct installation by running ``conda list`` in a Terminal
window, which should print the list of packages installed with Conda.
For Windows, if you accepted the default installation options, you didn't
check an option to add Conda to the PATH, so trying to run ``conda`` from
a regular ``Command Prompt`` will result in the following error: ``'conda'
is no recognized as an internal or external command, operatble program or
batch file``. That's to be expected. You will nee to launch an ``Anaconda
Prompt`` that was added at installation time to your list of programs
available from the Start menu.
Once either Conda or MiniConda has been set up you can install Catalyst:
1. Download the file `python2.7-environment.yml
<https://github.com/enigmampc/catalyst/blob/master/etc/python2.7-environment.yml>`_.
To download, simply click on the 'Raw' button and save the file locally
to a folder you can remember. Make sure that the file gets saved with the
``.yml`` extension, and nothing like a ``.txt`` file or anything else.
2. Open a Terminal window and enter [``cd/dir``] into the directory where you
saved the above ``python2.7-environment.yml`` file.
3. Install using this file. This step can take about 5-10 minutes to install.
.. code-block:: bash
conda env create -f python2.7-environment.yml
4. Activate the environment (which you need to do every time you start a new
session to run Catalyst):
**Linux or MacOS:**
.. code-block:: bash
source activate catalyst
**Windows:**
.. code-block:: bash
activate catalyst
5. Verify that Catalyst is install correctly:
.. code-block:: bash
catalyst --version
which should display the current version.
Congratulations! You now have Catalyst installed.
Troubleshooting ``conda`` Install
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the command ``conda env create -f python2.7-environment.yml`` in step 3
above failed for any reason, you can try setting up the environment manually
with the following steps:
1. If the above installation failed, and you have a partially set up catalyst
environment, remove it first. If you are starting from scratch, proceed to
step #2:
.. code-block:: bash
conda env remove --name catalyst
2. Create the environment:
.. code-block:: bash
conda create --name catalyst python=2.7 scipy zlib
3. Activate the environment:
**Linux or MacOS:**
.. code-block:: bash
source activate catalyst
**Windows:**
.. code-block:: bash
activate catalyst
4. Install the Catalyst inside the environment:
.. code-block:: bash
pip install enigma-catalyst matplotlib
5. Verify that Catalyst is installed correctly:
.. code-block:: bash
catalyst --version
which should display the current version.
Congratulations! You now have Catalyst properly installed.
.. _pip:
Installing with ``pip`` Installing with ``pip``
----------------------- -----------------------
@@ -28,15 +175,21 @@ Because LAPACK and the CPython headers are non-Python dependencies, the
correctway to install them varies from platform to platform. If you'd rather correctway to install them varies from platform to platform. If you'd rather
use a single tool to install Python and non-Python dependencies, or if you're use a single tool to install Python and non-Python dependencies, or if you're
already using `Anaconda <http://continuum.io/downloads>`_ as your Python already using `Anaconda <http://continuum.io/downloads>`_ as your Python
distribution, you can skip to the :ref:`Installing with Conda <conda>` distribution, refer to the :ref:`Installing with Conda <conda>` section.
section.
Once you've installed the necessary additional dependencies (see below for Once you've installed the necessary additional dependencies for your system
your particular platform), you should be able to simply run (see below for your particular platform: :ref:`Linux`, :ref:`MacOS` or
:ref:`Windows`), you should be able to simply run
.. code-block:: bash .. code-block:: bash
$ pip install enigma-catalyst $ pip install enigma-catalyst matplotlib
Note that in the command above we install two different packages. The second
one, ``matplotlib`` is a visualization library. While it's not strictly
required to run catalyst simulations or live trading, it comes in very handy
to visualize the performance of your algorithms, and for this reason we
recommend you install it, as well.
If you use Python for anything other than Catalyst, we **strongly** recommend If you use Python for anything other than Catalyst, we **strongly** recommend
that you install in a `virtualenv that you install in a `virtualenv
@@ -50,153 +203,7 @@ summarized version:
$ pip install virtualenv $ pip install virtualenv
$ virtualenv catalyst-venv $ virtualenv catalyst-venv
$ source ./catalyst-venv/bin/activate $ source ./catalyst-venv/bin/activate
$ pip install enigma-catalyst $ pip install enigma-catalyst matplotlib
Though not required by Catalyst directly, our example algorithms use
matplotlib to visually display the results of the trading algorithms. If you
wish to run any examples or use matplotlib during development, it can be
installed using:
.. code-block:: bash
$ pip install matplotlib
GNU/Linux
~~~~~~~~~
On `Debian-derived`_ Linux distributions, you can acquire all the necessary
binary dependencies from ``apt`` by running:
.. code-block:: bash
$ sudo apt-get install libatlas-base-dev python-dev gfortran pkg-config libfreetype6-dev
On recent `RHEL-derived`_ derived Linux distributions (e.g. Fedora), the
following should be sufficient to acquire the necessary additional
dependencies:
.. code-block:: bash
$ sudo dnf install atlas-devel gcc-c++ gcc-gfortran libgfortran python-devel redhat-rep-config
On `Arch Linux`_, you can acquire the additional dependencies via ``pacman``:
.. code-block:: bash
$ pacman -S lapack gcc gcc-fortran pkg-config
.. Commenting it out until Catalyst fully supports Python 3.X
..
.. There are also AUR packages available for installing `Python 3.4
.. <https://aur.archlinux.org/packages/python34/>`_ (Arch's default python is now
.. 3.5, but Catalyst only currently supports 3.4), and `ta-lib
.. <https://aur.archlinux.org/packages/ta-lib/>`_, an optional Catalyst dependency.
.. Python 2 is also installable via:
..
.. $ pacman -S python2
OSX
~~~
The version of Python shipped with OSX by default is generally out of date,
and has a number of quirks because it's used directly by the operating system.
For these reasons, many developers choose to install and use a separate Python
installation. The `Hitchhiker's Guide to Python`_ provides an excellent guide
to `Installing Python on OSX <http://docs.python-guide.org/en/latest/>`_,
which explains how to install Python with the `Homebrew`_ manager.
Assuming you've installed Python with Homebrew, you'll also likely need the
following brew packages:
.. code-block:: bash
$ brew install freetype pkg-config gcc openssl
OSX + virtualenv + matplotlib
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A note about using matplotlib in virtual enviroments on OSX: it may be
necessary to run
.. code-block:: bash
echo "backend: TkAgg" > ~/.matplotlib/matplotlibrc
in order to override the default ``macosx`` backend for your system, which
may not be accessible from inside the virtual environment. This will allow
Catalyst to open matplotlib charts from within a virtual environment, which
is useful for displaying the performance of your backtests. To learn more
about matplotlib backends, please refer to the
`matplotlib backend documentation <https://matplotlib.org/faq/usage_faq.html#what-is-a-backend>`_.
.. _windows:
Windows
~~~~~~~
In Windows, you will need the `Microsoft Visual C++ Compiler for Python 2.7
<https://www.microsoft.com/en-us/download/details.aspx?id=44266>`_. This
package contains the compiler and the set of system headers necessary for
producing binary wheels for Python 2.7 packages. If it's not already in your
system, download it and install it before proceeding to the next step.
For windows, the easiest and best supported way to install Catalyst is to use
:ref:`Conda <conda>`.
Some problems we have encountered installing the **Visual C++ Compiler**
mentioned above are as follows:
- **The system administrator has set policies to prevent this installation**.
In some systems, there is a default *Windows Software Restriction* policy
that prevents the installation of some software packages like this one.
You'll have to change the Registry to circumvent this:
- Click ``Start``, and search for ``regedit`` and launch the
``Registry Editor``
- Navigate to the following folder:
``HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Installer``
- If there is an entry for ``DisableMSI``, set the Value data to 0.
- If there is no such entry, click on the ``Edit`` menu -> ``New`` ->
``DWORD (32-bit) Value`` and enter ``DisableMSI`` as the Name (and by
default you get 0 as the Value Data)
|
- **The installer has encountered an unexpected error installing this package.
This may indicate a problem with this package. The error code is 2503.**
We have observed this when trying to install a package without enough
administrator permissions. Even when you are logged in as an Administrator,
you have to explictily install this package with administrator privileges:
- Click ``Start`` and find ``CMD`` or ``Command Prompt``
- Right click on it and choose ``Run as administrator``
- ``cd`` into the folder where you downloaded ``VCForPython27.msi``
- Run ``msiexec /i VCForPython27.msi``
Amazon Linux AMI
~~~~~~~~~~~~~~~~
The packages ``pip`` and ``setuptools`` that come shipped by default are very
outdated. Thus, you first need to run:
.. code-block:: bash
pip install --upgrade pip setuptools
The default installation is also missing the C and C++ compilers, which you
install by:
.. code-block:: bash
sudo yum install gcc gcc-c++
Then you should follow the regular installation instructions outlined at the
beginning of this page.
Troubleshooting ``pip`` Install Troubleshooting ``pip`` Install
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -287,99 +294,150 @@ Troubleshooting ``pip`` Install
sudo apt-get install python-dev sudo apt-get install python-dev
.. _conda: .. _linux:
Installing with ``conda`` GNU/Linux Requirements
------------------------- ----------------------
Another way to install Catalyst is via the ``conda`` package manager, which On `Debian-derived`_ Linux distributions, you can acquire all the necessary
comes as part of Continuum Analytics' `Anaconda binary dependencies from ``apt`` by running:
<http://continuum.io/downloads>`_ distribution.
The primary advantage of using Conda over ``pip`` is that conda natively .. code-block:: bash
understands the complex binary dependencies of packages like ``numpy`` and
``scipy``. This means that ``conda`` can install Catalyst and its
dependencies without requiring the use of a second tool to acquire Catalyst's
non-Python dependencies.
For Windows, you will need the *Microsoft Visual C++ Compiler for Python $ sudo apt-get install libatlas-base-dev python-dev gfortran pkg-config libfreetype6-dev
2.7*. Follow the instructions on the :ref:`Windows` section and come back
here.
For instructions on how to install ``conda``, see the `Conda Installation On recent `RHEL-derived`_ derived Linux distributions (e.g. Fedora), the
Documentation <http://conda.pydata.org/docs/download.html>`_. Alternatively, following should be sufficient to acquire the necessary additional
you can install MiniConda, which is a smaller footprint (fewer packages and dependencies:
smaller size) than its big brother Anaconda, but it still contains all the
main packages needed. To install MiniConda, you can follow these steps:
1. Download `MiniConda <https://conda.io/miniconda.html>`_. Select Python 2.7 .. code-block:: bash
for your Operating System.
2. Install MiniConda. See the `Installation Instructions
<https://conda.io/docs/user-guide/install/index.html>`_ if you need help.
3. Ensure the correct installation by running ``conda list`` in a Terminal
window, which should print the list of packages installed with Conda.
Once either Conda or MiniConda has been set up you can install Catalyst: $ sudo dnf install atlas-devel gcc-c++ gcc-gfortran libgfortran python-devel redhat-rep-config
1. Download the file `python2.7-environment.yml On `Arch Linux`_, you can acquire the additional dependencies via ``pacman``:
<https://github.com/enigmampc/catalyst/blob/master/etc/python2.7-environment.yml>`_.
2. Open a Terminal window and enter [``cd/dir``] into the directory where you
saved the above ``python2.7-environment.yml`` file.
3. Install using this file. This step can take about 5-10 minutes to install.
.. code-block:: bash .. code-block:: bash
conda env create -f python2.7-environment.yml $ pacman -S lapack gcc gcc-fortran pkg-config
4. Activate the environment (which you need to do every time you start a new .. Commenting it out until Catalyst fully supports Python 3.X
session to run Catalyst): ..
.. There are also AUR packages available for installing `Python 3.4
.. <https://aur.archlinux.org/packages/python34/>`_ (Arch's default python is now
.. 3.5, but Catalyst only currently supports 3.4), and `ta-lib
.. <https://aur.archlinux.org/packages/ta-lib/>`_, an optional Catalyst dependency.
.. Python 2 is also installable via:
**Linux or OSX:** ..
.. code-block:: bash .. $ pacman -S python2
source activate catalyst Amazon Linux AMI Notes
~~~~~~~~~~~~~~~~~~~~~~
**Windows:** The packages ``pip`` and ``setuptools`` that come shipped by default are very
outdated. Thus, you first need to run:
.. code-block:: bash .. code-block:: bash
activate catalyst pip install --upgrade pip setuptools
Congratulations! You now have Catalyst installed. The default installation is also missing the C and C++ compilers, which you
install by:
Troubleshooting ``conda`` Install .. code-block:: bash
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the command ``conda env create -f python2.7-environment.yml`` in step 3 sudo yum install gcc gcc-c++
above failed for any reason, you can try setting up the environment manually
with the following steps:
1. Create the environment: Then you should follow the regular installation instructions outlined at the
beginning of this page.
.. code-block:: bash
conda create --name catalyst python=2.7 scipy zlib .. _MacOS:
2. Activate the environment: MacOS Requirements
------------------
**Linux or OSX:** The version of Python shipped with MacOS by default is generally out of date,
and has a number of quirks because it's used directly by the operating system.
For these reasons, many developers choose to install and use a separate Python
installation. The `Hitchhiker's Guide to Python`_ provides an excellent guide
to `Installing Python on MacOS <http://docs.python-guide.org/en/latest/>`_,
which explains how to install Python with the `Homebrew`_ manager.
.. code-block:: bash Assuming you've installed Python with Homebrew, you'll also likely need the
following brew packages:
source activate catalyst .. code-block:: bash
**Windows:** $ brew install freetype pkg-config gcc openssl
.. code-block:: bash MacOS + virtualenv + matplotlib
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
activate catalyst A note about using matplotlib in virtual enviroments on MacOS: it may be
necessary to run
3. Install the Catalyst inside the environment: .. code-block:: bash
.. code-block:: bash echo "backend: TkAgg" > ~/.matplotlib/matplotlibrc
pip install enigma-catalyst matplotlib in order to override the default ``MacOS`` backend for your system, which
may not be accessible from inside the virtual environment. This will allow
Catalyst to open matplotlib charts from within a virtual environment, which
is useful for displaying the performance of your backtests. To learn more
about matplotlib backends, please refer to the
`matplotlib backend documentation <https://matplotlib.org/faq/usage_faq.html#what-is-a-backend>`_.
.. _windows:
Windows Requirements
--------------------
In Windows, you will first need to install the `Microsoft Visual C++ Compiler
for Python 2.7
<https://www.microsoft.com/en-us/download/details.aspx?id=44266>`_. This
package contains the compiler and the set of system headers necessary for
producing binary wheels for Python 2.7 packages. If it's not already in your
system, download it and install it before proceeding to the next step.
Once you have the above compiler installed, the easiest and best supported way
to install Catalyst in Windows is to use :ref:`Conda <conda>`. If you didn't
any problems installing the compiler, jump to the :ref:`Conda <conda>` section,
otherwise keep on reading to troubleshoot the C++ compiler installtion.
Some problems we have encountered installing the **Visual C++ Compiler**
mentioned above are as follows:
- **The system administrator has set policies to prevent this installation**.
In some systems, there is a default *Windows Software Restriction* policy
that prevents the installation of some software packages like this one.
You'll have to change the Registry to circumvent this:
- Click ``Start``, and search for ``regedit`` and launch the
``Registry Editor``
- Navigate to the following folder:
``HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Installer``
- If the last folder does not exist, create it by right-clicking on the
parent folder and choosing -> ``New`` -> ``Key`` and typing ``Installer``
- If there is an entry for ``DisableMSI``, set the Value data to 0.
- If there is no such entry, click on the ``Edit`` menu -> ``New`` ->
``DWORD (32-bit) Value`` and enter ``DisableMSI`` as the Name (and by
default you get 0 as the Value Data)
|
- **The installer has encountered an unexpected error installing this package.
This may indicate a problem with this package. The error code is 2503.**
We have observed this when trying to install a package without enough
administrator permissions. Even when you are logged in as an Administrator,
you have to explictily install this package with administrator privileges:
- Click ``Start`` and find ``CMD`` or ``Command Prompt``
- Right click on it and choose ``Run as administrator``
- ``cd`` into the folder where you downloaded ``VCForPython27.msi``
- Run ``msiexec /i VCForPython27.msi``
Getting Help Getting Help
------------ ------------
+77 -19
View File
@@ -2,9 +2,46 @@
Release Notes Release Notes
============= =============
Version 0.3.8
^^^^^^^^^^^^^
**Release Date**: 2017-11-14
Bug Fixes
~~~~~~~~~
- Fixed a warning filter issue introduced with the latest release
Version 0.3.7
^^^^^^^^^^^^^
**Release Date**: 2017-11-14
Bug Fixes
~~~~~~~~~
- Fixed an SSL cert issue (:issue:`64`)
- Fixed cumulative stats warnings (:issue:`63`)
- Disabled auto-ingestion because of unresolved caching issues (:issue:`47`)
- Standardized live-trading stats (:issue:`61`)
Build
~~~~~
- Added a mean-reversion sample algo
- Added minutely stats in the analyze() function (:issue:`62`)
- Added specificity to some error messages
Version 0.3.6
^^^^^^^^^^^^^
**Release Date**: 2017-11-4
Bug Fixes
~~~~~~~~~
- Fixed an issue with single bar data.history() (:issue:`55`)
Version 0.3.5 Version 0.3.5
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
**Release Date**: 2017-11-2 **Release Date**: 2017-11-4
Bug Fixes Bug Fixes
~~~~~~~~~ ~~~~~~~~~
@@ -22,7 +59,8 @@ Bug Fixes
- Fixed issue with sell orders in backtesting - Fixed issue with sell orders in backtesting
- Fixed data frequency issues with data.history() in backtesting - Fixed data frequency issues with data.history() in backtesting
- Fixed an issue with can_trade() - Fixed an issue with can_trade()
- Reduced the commission and slippage values to account for lower volume transactions - Reduced the commission and slippage values to account for lower volume
transactions
Build Build
~~~~~ ~~~~~
@@ -33,12 +71,18 @@ Documentation
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
- Improved installation notes for Windows C++ compiler and Conda - Improved installation notes for Windows C++ compiler and Conda
- Addition of `Jupyter Notebook guide <https://enigmampc.github.io/catalyst/jupyter.html>`_ - Addition of
- Addition of `Live Trading page <https://enigmampc.github.io/catalyst/live-trading.html>`_ `Jupyter Notebook guide <https://enigmampc.github.io/catalyst/jupyter.html>`_
- Addition of `Videos page <https://enigmampc.github.io/catalyst/videos.html>`_ - Addition of
- Addition of `Resources page <https://enigmampc.github.io/catalyst/resources.html>`_ `Live Trading page <https://enigmampc.github.io/catalyst/live-trading.html>`_
- Addition of `Development Guidelines <https://enigmampc.github.io/catalyst/development-guidelines.html>`_ - Addition of
- Addition of `Release Notes <https://enigmampc.github.io/catalyst/releases.html>`_ `Videos page <https://enigmampc.github.io/catalyst/videos.html>`_
- Addition of
`Resources page <https://enigmampc.github.io/catalyst/resources.html>`_
- Addition of `Development Guidelines
<https://enigmampc.github.io/catalyst/development-guidelines.html>`_
- Addition of
`Release Notes <https://enigmampc.github.io/catalyst/releases.html>`_
- Updated code docstrings - Updated code docstrings
@@ -88,9 +132,11 @@ Bug Fixes
~~~~~~~~~ ~~~~~~~~~
- Fixed OS-dependent path issue in data bundle - Fixed OS-dependent path issue in data bundle
- Changed handling of empty ``auth.json``, instead of throwing an error for missing file - Changed handling of empty ``auth.json``, instead of throwing an error for
missing file
- Updated ``etc/python2.7-environment.yml`` to work with Catalyst version 0.3 - Updated ``etc/python2.7-environment.yml`` to work with Catalyst version 0.3
- Updated ``catalyst/examples/buy_and_hodl.py`` and ``catalyst/examples/buy_low_sell_high.py`` to work with Catalyst version 0.3 - Updated ``catalyst/examples/buy_and_hodl.py`` and
``catalyst/examples/buy_low_sell_high.py`` to work with Catalyst version 0.3
Version 0.3 Version 0.3
@@ -109,15 +155,19 @@ Version 0.2.dev5
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
**Release Date**: 2017-10-03 **Release Date**: 2017-10-03
- Fixes bug in data.history function that was formatting 'volume' data as integers, now they are returned as floats with up to 9 decimals of precision. Data bundles redone. - Fixes bug in data.history function that was formatting 'volume' data as
integers, now they are returned as floats with up to 9 decimals of precision.
Data bundles redone.
Version 0.2.dev4 Version 0.2.dev4
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
**Release Date**: 2017-09-20 **Release Date**: 2017-09-20
- Fixes bug in the pricing resolution of 1-minute data, now set to 8 decimal places. Pricing resolution of daily data remains set to 9 decimal places. - Fixes bug in the pricing resolution of 1-minute data, now set to 8 decimal
- The current data bundle takes 340MB compressed for download, and 460MB uncompressed on disk for Catalyst to use. places. Pricing resolution of daily data remains set to 9 decimal places.
- The current data bundle takes 340MB compressed for download, and 460MB
uncompressed on disk for Catalyst to use.
Version 0.2.dev3 Version 0.2.dev3
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
@@ -126,9 +176,12 @@ Version 0.2.dev3
- 1-minute resolution OHLCV data bundle for backtesting from Poloniex exchange - 1-minute resolution OHLCV data bundle for backtesting from Poloniex exchange
- Implementation of trading of fractional crypto assets (i.e. 0.01 BTC) - Implementation of trading of fractional crypto assets (i.e. 0.01 BTC)
- Minimum trade size of a coin can be configured on a per-coin basis, defaults to 0.00000001 in backtesting (most exchanges set the minimum trade to larger amounts, which will impact live trading) - Minimum trade size of a coin can be configured on a per-coin basis, defaults
to 0.00000001 in backtesting (most exchanges set the minimum trade to larger
amounts, which will impact live trading)
- Increased pricing resolution from 3 to 9 decimal places - Increased pricing resolution from 3 to 9 decimal places
- The current data bundle takes 40MB compressed for download, and 99MB uncompressed on disk for Catalyst to use. - The current data bundle takes 40MB compressed for download, and 99MB
uncompressed on disk for Catalyst to use.
Version 0.2.dev2 Version 0.2.dev2
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
@@ -146,19 +199,24 @@ Version 0.2.dev1
- Comprehensive trading functionality against exchanges Bitfinex and Bittrex. - Comprehensive trading functionality against exchanges Bitfinex and Bittrex.
- Support for all trading pairs available on each exchange. - Support for all trading pairs available on each exchange.
- Multiple algorithms can trade simultaneously against a single exchange using the same account. - Multiple algorithms can trade simultaneously against a single exchange
- Each algorithm has a persisted state (i.e. algorithm can be stopped and restarted preserving the state without data loss) that tracks all open orders, executed transactions and portfolio positions. using the same account.
- Each algorithm has a persisted state (i.e. algorithm can be stopped and
restarted preserving the state without data loss) that tracks all open
orders, executed transactions and portfolio positions.
- Minute by minute portfolio performance metrics. - Minute by minute portfolio performance metrics.
- Daily summary performance statistics compatible with pyfolio, a Python library for performance and risk analysis of financial portfolios - Daily summary performance statistics compatible with pyfolio, a Python
library for performance and risk analysis of financial portfolios
Version 0.1.dev9 Version 0.1.dev9
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
**Release Date**: 2017-08-28 **Release Date**: 2017-08-28
- Retrieval of crypto benchmark from bundle, instead of hitting Poloniex exchange directly - Retrieval of crypto benchmark from bundle, instead of hitting Poloniex
exchange directly
- Change of bundle storage provider from Dropbox to AWS - Change of bundle storage provider from Dropbox to AWS
- Fix issue with 1/1000 scaling issue of prices in bundle - Fix issue with 1/1000 scaling issue of prices in bundle
+149
View File
@@ -0,0 +1,149 @@
Utilities
=========
This section covers a variety of utilites that provide complimentary
functionality to your trading algorithms. These are code snippets that you can
add to any algorithm to add the desired functionality.
If you are looking for example trading algorithms, see the corresponding section.
Output to CSV file
~~~~~~~~~~~~~~~~~~
Add this script to the analyze method to create and save a CSV file with the
results from the trading algorithm. This file will include the default
parameters of the results DataFrame plus any recorded variables and will be
saved in the same location where your trading algorithm is saved. The exact
script that you need to use depends on the interface that you are using to run
your trading algorithm, which could be the CLI or a Python Interpreter.
1. Script to use with CLI:
.. code-block:: python
def analyze(context=None, results=None):
import sys
import os
from os.path import basename
# Save results in CSV file
filename = os.path.splitext(basename(sys.argv[3]))[0]
results.to_csv(filename + '.csv')
2. Script to use with Python Interpreter:
.. code-block:: python
def analyze(context=None, results=None):
import os
from os.path import basename
# Save results in CSV file
filename = os.path.splitext(os.path.basename(__file__))[0]
results.to_csv(filename + '.csv')
Extracting market data
~~~~~~~~~~~~~~~~~~~~~~
Use this script to save the price and volume data of one cryptoasset in a CSV
file, which will be saved in the same location and with the same name as your
Python file. To get custom data, simply modify the asset's symbol and the dates.
Run this script directly from your development environment: python scriptname.py,
where the contents of 'scriptname.py' are as follows. Two different version are
provided as an example for daily- and minute-resolution data respectively:
Simpler case for daily data
.. code-block:: python
import os
import pytz
from datetime import datetime
from catalyst.api import record, symbol, symbols
from catalyst.utils.run_algo import run_algorithm
def initialize(context):
# Portfolio assets list
context.asset = symbol('btc_usdt') # Bitcoin on Poloniex
def handle_data(context, data):
# Variables to record for a given asset: price and volume
price = data.current(context.asset, 'price')
volume = data.current(context.asset, 'volume')
record(price=price, volume=volume)
def analyze(context=None, results=None):
# Generate DataFrame with Price and Volume only
data = results[['price','volume']]
# Save results in CSV file
filename = os.path.splitext(os.path.basename(__file__))[0]
data.to_csv(filename + '.csv')
''' Bitcoin data is available on Poloniex since 2015-3-1.
Dates vary for other tokens. In the example below, we choose the
full month of July of 2017.
'''
start = datetime(2017, 1, 1, 0, 0, 0, 0, pytz.utc)
end = datetime(2017, 7, 31, 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=10000,
base_currency = 'usdt')
More versatile case for minute data
.. code-block:: python
import os
import csv
import pytz
from datetime import datetime
from catalyst.api import record, symbol, symbols
from catalyst.utils.run_algo import run_algorithm
def initialize(context):
# Portfolio assets list
context.asset = symbol('btc_usdt') # Bitcoin on Poloniex
# Creates a .CSV file with the same name as this script to store results
context.csvfile = open(os.path.splitext(
os.path.basename(__file__))[0]+'.csv', 'w+')
context.csvwriter = csv.writer(context.csvfile)
def handle_data(context, data):
# Variables to record for a given asset: price and volume
# Other options include 'open', 'high', 'open', 'close'
# Please note that 'price' equals 'close'
date = context.blotter.current_dt # current time in each iteration
price = data.current(context.asset, 'price')
volume = data.current(context.asset, 'volume')
# Writes one line to CSV on each iteration with the chosen variables
context.csvwriter.writerow([date,price,volume])
def analyze(context=None, results=None):
# Close open file properly at the end
context.csvfile.close()
# Bitcoin data is available from 2015-3-2. Dates vary for other tokens.
start = datetime(2017, 7, 30, 0, 0, 0, 0, pytz.utc)
end = datetime(2017, 7, 31, 0, 0, 0, 0, pytz.utc)
results = run_algorithm(initialize=initialize,
handle_data=handle_data,
analyze=analyze,
start=start,
end=end,
exchange_name='poloniex',
data_frequency='minute',
base_currency ='usdt',
capital_base=10000 )
+17 -1
View File
@@ -23,4 +23,20 @@ Where things go smoothly:
| |
Where things don't: Where things don't:
Coming up next! .. raw:: html
<iframe width="560" height="315" src="https://www.youtube.com/embed/qLkQcWlUBy8" frameborder="0" allowfullscreen></iframe>
|
|
Backtesting a Strategy
----------------------
This algorithm is based on a simple momentum strategy. When the cryptoasset
goes up quickly, were going to buy; when it goes down quickly, were going to
sell. Hopefully, well ride the waves.
.. raw:: html
<iframe width="560" height="315" src="https://www.youtube.com/embed/JOBRwst9jUY" frameborder="0" allowfullscreen></iframe>
+56 -24
View File
@@ -42,17 +42,16 @@ class TestExchangeBundle:
def test_ingest_minute(self): def test_ingest_minute(self):
data_frequency = 'minute' data_frequency = 'minute'
exchange_name = 'bitfinex' exchange_name = 'poloniex'
exchange = get_exchange(exchange_name) exchange = get_exchange(exchange_name)
exchange_bundle = ExchangeBundle(exchange) exchange_bundle = ExchangeBundle(exchange)
assets = [ assets = [
exchange.get_asset('xmr_btc') exchange.get_asset('eth_btc')
] ]
# start = pd.to_datetime('2017-09-01', utc=True) start = pd.to_datetime('2016-03-01', utc=True)
start = pd.to_datetime('2016-01-01', utc=True) end = pd.to_datetime('2017-11-1', utc=True)
end = pd.to_datetime('2017-9-30', utc=True)
log.info('ingesting exchange bundle {}'.format(exchange_name)) log.info('ingesting exchange bundle {}'.format(exchange_name))
exchange_bundle.ingest( exchange_bundle.ingest(
@@ -122,8 +121,8 @@ class TestExchangeBundle:
def test_ingest_daily(self): def test_ingest_daily(self):
exchange_name = 'bitfinex' exchange_name = 'bitfinex'
data_frequency = 'daily' data_frequency = 'minute'
include_symbols = 'btc_usd' include_symbols = 'neo_btc'
# exchange_name = 'poloniex' # exchange_name = 'poloniex'
# data_frequency = 'daily' # data_frequency = 'daily'
@@ -422,7 +421,8 @@ class TestExchangeBundle:
data_frequency=data_frequency, data_frequency=data_frequency,
asset=asset, asset=asset,
writer=writer, writer=writer,
empty_rows_behavior='raise' empty_rows_behavior='raise',
duplicates_behavior='raise'
) )
bundle_series = bundle.get_history_window_series( bundle_series = bundle.get_history_window_series(
@@ -438,26 +438,30 @@ class TestExchangeBundle:
pass pass
def main_bundle_to_csv(self): def main_bundle_to_csv(self):
exchange_name = 'bitfinex' exchange_name = 'poloniex'
data_frequency = 'minute' data_frequency = 'minute'
exchange = get_exchange(exchange_name) exchange = get_exchange(exchange_name)
asset = exchange.get_asset('neo_usd') asset = exchange.get_asset('eth_btc')
start_dt = pd.to_datetime('2016-5-31', utc=True)
end_dt = pd.to_datetime('2016-6-1', utc=True)
self._bundle_to_csv( self._bundle_to_csv(
asset=asset, asset=asset,
exchange=exchange, exchange_name=exchange.name,
data_frequency=data_frequency, data_frequency=data_frequency,
filename='{}_{}_{}'.format( filename='{}_{}_{}'.format(
exchange_name, data_frequency, asset.symbol exchange_name, data_frequency, asset.symbol
) ),
start_dt=start_dt,
end_dt=end_dt
) )
def bundle_to_csv(self): def bundle_to_csv(self):
exchange_name = 'bitfinex' exchange_name = 'poloniex'
data_frequency = 'minute' data_frequency = 'minute'
period = '2017-10' period = '2017-01'
symbol = 'neo_btc' symbol = 'eth_btc'
exchange = get_exchange(exchange_name) exchange = get_exchange(exchange_name)
asset = exchange.get_asset(symbol) asset = exchange.get_asset(symbol)
@@ -470,20 +474,23 @@ class TestExchangeBundle:
) )
self._bundle_to_csv( self._bundle_to_csv(
asset=asset, asset=asset,
exchange=exchange, exchange_name=exchange.name,
data_frequency=data_frequency, data_frequency=data_frequency,
path=path, path=path,
filename=period filename=period
) )
pass pass
def _bundle_to_csv(self, asset, exchange, data_frequency, filename, def _bundle_to_csv(self, asset, exchange_name, data_frequency, filename,
path=None): path=None, start_dt=None, end_dt=None):
bundle = ExchangeBundle(exchange) bundle = ExchangeBundle(exchange_name)
reader = bundle.get_reader(data_frequency, path=path) reader = bundle.get_reader(data_frequency, path=path)
start_dt = reader.first_trading_day if start_dt is None:
end_dt = reader.last_available_dt start_dt = reader.first_trading_day
if end_dt is None:
end_dt = reader.last_available_dt
if data_frequency == 'daily': if data_frequency == 'daily':
end_dt = end_dt - pd.Timedelta(hours=23, minutes=59) end_dt = end_dt - pd.Timedelta(hours=23, minutes=59)
@@ -507,14 +514,39 @@ class TestExchangeBundle:
df = get_df_from_arrays(arrays, periods) df = get_df_from_arrays(arrays, periods)
folder = os.path.join( folder = os.path.join(
tempfile.gettempdir(), 'catalyst', exchange.name, asset.symbol tempfile.gettempdir(), 'catalyst', exchange_name, asset.symbol
) )
ensure_directory(folder) ensure_directory(folder)
path = os.path.join(folder, filename + '.csv') path = os.path.join(folder, filename + '.csv')
log.info('creating csv file: {}'.format(path)) log.info('creating csv file: {}'.format(path))
print('HEAD\n{}'.format(df.head(10))) print('HEAD\n{}'.format(df.head(100)))
print('TAIL\n{}'.format(df.tail(10))) print('TAIL\n{}'.format(df.tail(100)))
df.to_csv(path) df.to_csv(path)
pass pass
def test_ingest_csv(self):
data_frequency = 'minute'
exchange_name = 'bittrex'
path = '/Users/fredfortier/Dropbox/Enigma/Data/bittrex_bat_eth.csv'
exchange_bundle = ExchangeBundle(exchange_name)
exchange_bundle.ingest_csv(path, data_frequency)
exchange = get_exchange(exchange_name)
asset = exchange.get_asset('bat_eth')
start_dt = pd.to_datetime('2017-6-3', utc=True)
end_dt = pd.to_datetime('2017-8-3 19:24', utc=True)
self._bundle_to_csv(
asset=asset,
exchange_name=exchange.name,
data_frequency=data_frequency,
filename='{}_{}_{}'.format(
exchange_name, data_frequency, asset.symbol
),
start_dt=start_dt,
end_dt=end_dt
)
pass
+7 -7
View File
@@ -1,16 +1,13 @@
import pandas as pd import pandas as pd
from catalyst.exchange.exchange_data_portal import DataPortalExchangeBacktest, \
DataPortalExchangeLive
from logbook import Logger from logbook import Logger
from test_utils import rnd_history_date_days, rnd_bar_count
from catalyst import get_calendar from catalyst import get_calendar
from catalyst.exchange.asset_finder_exchange import AssetFinderExchange from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
from catalyst.exchange.bitfinex.bitfinex import Bitfinex from catalyst.exchange.exchange_data_portal import DataPortalExchangeBacktest, \
from catalyst.exchange.bittrex.bittrex import Bittrex DataPortalExchangeLive
from catalyst.exchange.exchange_utils import get_exchange_auth, \ from catalyst.exchange.exchange_utils import get_common_assets
get_common_assets
from catalyst.exchange.factory import get_exchange, get_exchanges from catalyst.exchange.factory import get_exchange, get_exchanges
from test_utils import rnd_history_date_days, rnd_bar_count, output_df
log = Logger('test_bitfinex') log = Logger('test_bitfinex')
@@ -113,3 +110,6 @@ class TestExchangeDataPortal:
) )
log.info('found history window: {}'.format(data)) log.info('found history window: {}'.format(data))
def test_validate_resample(self):
pass
+14 -11
View File
@@ -1,9 +1,10 @@
from catalyst.exchange.bittrex.bittrex import Bittrex
from catalyst.exchange.poloniex.poloniex import Poloniex from catalyst.exchange.poloniex.poloniex import Poloniex
from catalyst.finance.order import Order from catalyst.finance.order import Order
from base import BaseExchangeTestCase from base import BaseExchangeTestCase
from logbook import Logger from logbook import Logger
from catalyst.exchange.exchange_utils import get_exchange_auth from catalyst.exchange.exchange_utils import get_exchange_auth
import pandas as pd
from test_utils import output_df
log = Logger('test_poloniex') log = Logger('test_poloniex')
@@ -51,18 +52,20 @@ class TestPoloniex(BaseExchangeTestCase):
def test_get_candles(self): def test_get_candles(self):
log.info('retrieving candles') log.info('retrieving candles')
ohlcv_neo = self.exchange.get_candles( assets = self.exchange.get_asset('eth_btc')
ohlcv = self.exchange.get_candles(
# end_dt=pd.to_datetime('2017-11-01', utc=True),
end_dt=None,
freq='5T', freq='5T',
assets=self.exchange.get_asset('eth_btc') assets=assets,
) bar_count=200
ohlcv_neo_ubq = self.exchange.get_candles(
freq='5T',
assets=[
self.exchange.get_asset('neos_btc'),
self.exchange.get_asset('via_btc')
],
bar_count=14
) )
df = pd.DataFrame(ohlcv)
df.set_index('last_traded', drop=True, inplace=True)
log.info(df.tail(25))
path = output_df(df, assets, '5min_candles')
log.info('saved candles: {}'.format(path))
pass pass
def test_tickers(self): def test_tickers(self):
+52 -3
View File
@@ -1,17 +1,66 @@
import os
import tempfile
from datetime import timedelta from datetime import timedelta
from random import randint from random import randint
import pandas as pd import pandas as pd
from catalyst.assets._assets import TradingPair
from catalyst.utils.paths import ensure_directory
def rnd_history_date_days(max_days=30): def rnd_history_date_days(max_days=30, last_dt=None):
now = pd.Timestamp.utcnow() if last_dt is None:
last_dt = pd.Timestamp.utcnow()
days = randint(0, max_days) days = randint(0, max_days)
return now - timedelta(days=days) return last_dt - timedelta(days=days)
def rnd_history_date_minutes(max_minutes=1440):
now = pd.Timestamp.utcnow()
days = randint(0, max_minutes)
return now - timedelta(minutes=days)
def rnd_bar_count(max_bars=21): def rnd_bar_count(max_bars=21):
now = pd.Timestamp.utcnow() now = pd.Timestamp.utcnow()
return randint(0, max_bars) return randint(0, max_bars)
def output_df(df, assets, name=None):
"""
Outputs a price DataFrame to a temp folder.
Parameters
----------
df: pd.DataFrame
assets
name
Returns
-------
"""
if isinstance(assets, TradingPair):
exchange_folder = assets.exchange
asset_folder = assets.symbol
else:
exchange_folder = ','.join([asset.exchange for asset in assets])
asset_folder = ','.join([asset.symbol for asset in assets])
folder = os.path.join(
tempfile.gettempdir(), 'catalyst', exchange_folder, asset_folder
)
ensure_directory(folder)
if name is None:
name = 'output'
path = os.path.join(folder, '{}.csv'.format(name))
df.to_csv(path)
return path