mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-29 11:18:20 +08:00
Compare commits
2
Commits
0.4.1
..
unit-tests
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c571c1c2ae | ||
|
|
f279101393 |
+3
-3
@@ -11,13 +11,13 @@
|
|||||||
#
|
#
|
||||||
# https://127.0.0.1
|
# https://127.0.0.1
|
||||||
#
|
#
|
||||||
# Default password is 'jupyter'. To provide another, see:
|
# default password is jupyter. to provide another, see:
|
||||||
# http://jupyter-notebook.readthedocs.org/en/latest/public_server.html#preparing-a-hashed-password
|
# http://jupyter-notebook.readthedocs.org/en/latest/public_server.html#preparing-a-hashed-password
|
||||||
#
|
#
|
||||||
# Once generated, you can pass the new value via `docker run --env` the first time
|
# once generated, you can pass the new value via `docker run --env` the first time
|
||||||
# you start the container.
|
# you start the container.
|
||||||
#
|
#
|
||||||
# You can also run an algo using the docker exec command. For example:
|
# You can also run an algo using the docker exec command. For example:
|
||||||
#
|
#
|
||||||
# docker exec -it catalyst catalyst run -f /projects/my_algo.py --start 2015-1-1 --end 2016-1-1 /projects/result.pickle
|
# docker exec -it catalyst catalyst run -f /projects/my_algo.py --start 2015-1-1 --end 2016-1-1 /projects/result.pickle
|
||||||
#
|
#
|
||||||
|
|||||||
+4
-4
@@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Note: the dev build requires a quantopian/catalyst image, which you can build as follows:
|
# Note: the dev build requires a quantopian/catalyst image, which you can build as follows:
|
||||||
#
|
#
|
||||||
# docker build -t quantopian/catalyst -f Dockerfile .
|
# docker build -t quantopian/catalyst -f Dockerfile
|
||||||
#
|
#
|
||||||
# To run the container:
|
# To run the container:
|
||||||
#
|
#
|
||||||
@@ -15,13 +15,13 @@
|
|||||||
#
|
#
|
||||||
# https://127.0.0.1
|
# https://127.0.0.1
|
||||||
#
|
#
|
||||||
# Default password is 'jupyter'. To provide another, see:
|
# default password is jupyter. to provide another, see:
|
||||||
# http://jupyter-notebook.readthedocs.org/en/latest/public_server.html#preparing-a-hashed-password
|
# http://jupyter-notebook.readthedocs.org/en/latest/public_server.html#preparing-a-hashed-password
|
||||||
#
|
#
|
||||||
# Once generated, you can pass the new value via `docker run --env` the first time
|
# once generated, you can pass the new value via `docker run --env` the first time
|
||||||
# you start the container.
|
# you start the container.
|
||||||
#
|
#
|
||||||
# You can also run an algo using the docker exec command. For example:
|
# You can also run an algo using the docker exec command. For example:
|
||||||
#
|
#
|
||||||
# docker exec -it catalystdev catalyst run -f /projects/my_algo.py --start 2015-1-1 --end 2016-1-1 /projects/result.pickle
|
# docker exec -it catalystdev catalyst run -f /projects/my_algo.py --start 2015-1-1 --end 2016-1-1 /projects/result.pickle
|
||||||
#
|
#
|
||||||
|
|||||||
+3
-72
@@ -1,72 +1,3 @@
|
|||||||
.. image:: https://s3.amazonaws.com/enigmaco-docs/enigma-catalyst.jpg
|
All the documentation for `Catalyst <https://github.com/enigmampc/catalyst>`_
|
||||||
:target: https://enigmampc.github.io/catalyst
|
can be found in the
|
||||||
:align: center
|
`documentation website <https://enigmampc.github.io/catalyst>`_.
|
||||||
:alt: Enigma | Catalyst
|
|
||||||
|
|
||||||
|version tag|
|
|
||||||
|version status|
|
|
||||||
|discord|
|
|
||||||
|twitter|
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
Catalyst is an algorithmic trading library for crypto-assets written in Python.
|
|
||||||
It allows trading strategies to be easily expressed and backtested against
|
|
||||||
historical data (with daily and minute resolution), providing analytics and
|
|
||||||
insights regarding a particular strategy's performance. Catalyst also supports
|
|
||||||
live-trading of crypto-assets starting with three exchanges (Bitfinex, Bittrex,
|
|
||||||
and Poloniex) with more being added over time. Catalyst empowers users to share
|
|
||||||
and curate data and build profitable, data-driven investment strategies. Please
|
|
||||||
visit `enigma.co <https://www.enigma.co>`_ to learn more about Catalyst, or
|
|
||||||
refer to the `whitepaper <https://www.enigma.co/enigma_catalyst.pdf>`_ for
|
|
||||||
further technical details.
|
|
||||||
|
|
||||||
Catalyst builds on top of the well-established
|
|
||||||
`Zipline <https://github.com/quantopian/zipline>`_ project. We did our best to
|
|
||||||
minimize structural changes to the general API to maximize compatibility with
|
|
||||||
existing trading algorithms, developer knowledge, and tutorials. Join us on
|
|
||||||
`Discord <https://discord.gg/SJK32GY>`_ where we have a *#catalyst_dev* channel
|
|
||||||
for questions around Catalyst, algorithmic trading and technical support.
|
|
||||||
|
|
||||||
Overview
|
|
||||||
========
|
|
||||||
|
|
||||||
- Ease of use: Catalyst tries to get out of your way so that you can
|
|
||||||
focus on algorithm development. See
|
|
||||||
`examples of trading strategies <https://github.com/enigmampc/catalyst/tree/master/catalyst/examples>`_
|
|
||||||
provided.
|
|
||||||
- Support for several of the top crypto-exchanges by trading volume:
|
|
||||||
`Bitfinex <https://www.bitfinex.com>`_, `Bittrex <http://www.bittrex.com>`_,
|
|
||||||
and `Poloniex <https://www.poloniex.com>`_.
|
|
||||||
- Secure: You and only you have access to each exchange API keys for your accounts.
|
|
||||||
- Input of historical pricing data of all crypto-assets by exchange,
|
|
||||||
with daily and minute resolution. See
|
|
||||||
`Catalyst Market Coverage Overview <https://www.enigma.co/catalyst/status>`_.
|
|
||||||
- Backtesting and live-trading functionality, with a seamless transition
|
|
||||||
between the two modes.
|
|
||||||
- Output of performance statistics are based on Pandas DataFrames to
|
|
||||||
integrate nicely into the existing PyData eco-system.
|
|
||||||
- Statistic and machine learning libraries like matplotlib, scipy,
|
|
||||||
statsmodels, and sklearn support development, analysis, and
|
|
||||||
visualization of state-of-the-art trading systems.
|
|
||||||
- Addition of Bitcoin price (btc_usdt) as a benchmark for comparing
|
|
||||||
performance across trading algorithms.
|
|
||||||
|
|
||||||
Go to our `Documentation Website <https://enigmampc.github.io/catalyst/>`_.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. |version tag| image:: https://img.shields.io/pypi/v/enigma-catalyst.svg
|
|
||||||
:target: https://pypi.python.org/pypi/enigma-catalyst
|
|
||||||
|
|
||||||
.. |version status| image:: https://img.shields.io/pypi/pyversions/enigma-catalyst.svg
|
|
||||||
:target: https://pypi.python.org/pypi/enigma-catalyst
|
|
||||||
|
|
||||||
.. |discord| image:: https://img.shields.io/badge/discord-join%20chat-green.svg
|
|
||||||
:target: https://discordapp.com/invite/SJK32GY
|
|
||||||
|
|
||||||
.. |twitter| image:: https://img.shields.io/twitter/follow/enigmampc.svg?style=social&label=Follow&style=flat-square
|
|
||||||
:target: https://twitter.com/enigmampc
|
|
||||||
|
|
||||||
|
|
||||||
+10
-4
@@ -29,14 +29,11 @@ from ._version import get_versions
|
|||||||
from . algorithm import TradingAlgorithm
|
from . algorithm import TradingAlgorithm
|
||||||
from . import api
|
from . import api
|
||||||
|
|
||||||
from catalyst.utils.calendars.calendar_utils import global_calendar_dispatcher
|
|
||||||
|
|
||||||
__version__ = get_versions()['version']
|
|
||||||
del get_versions
|
|
||||||
|
|
||||||
# PERF: Fire a warning if calendars were instantiated during catalyst import.
|
# PERF: Fire a warning if calendars were instantiated during catalyst import.
|
||||||
# Having calendars doesn't break anything per-se, but it makes catalyst imports
|
# Having calendars doesn't break anything per-se, but it makes catalyst imports
|
||||||
# noticeably slower, which becomes particularly noticeable in the Zipline CLI.
|
# noticeably slower, which becomes particularly noticeable in the Zipline CLI.
|
||||||
|
from catalyst.utils.calendars.calendar_utils import global_calendar_dispatcher
|
||||||
if global_calendar_dispatcher._calendars:
|
if global_calendar_dispatcher._calendars:
|
||||||
import warnings
|
import warnings
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
@@ -47,6 +44,10 @@ if global_calendar_dispatcher._calendars:
|
|||||||
del global_calendar_dispatcher
|
del global_calendar_dispatcher
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = get_versions()['version']
|
||||||
|
del get_versions
|
||||||
|
|
||||||
|
|
||||||
def load_ipython_extension(ipython):
|
def load_ipython_extension(ipython):
|
||||||
from .__main__ import catalyst_magic
|
from .__main__ import catalyst_magic
|
||||||
ipython.register_magic_function(catalyst_magic, 'line_cell', 'catalyst')
|
ipython.register_magic_function(catalyst_magic, 'line_cell', 'catalyst')
|
||||||
@@ -68,6 +69,7 @@ if os.name == 'nt':
|
|||||||
_()
|
_()
|
||||||
del _
|
del _
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'TradingAlgorithm',
|
'TradingAlgorithm',
|
||||||
'api',
|
'api',
|
||||||
@@ -78,3 +80,7 @@ __all__ = [
|
|||||||
'run_algorithm',
|
'run_algorithm',
|
||||||
'utils',
|
'utils',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
from ._version import get_versions
|
||||||
|
__version__ = get_versions()['version']
|
||||||
|
del get_versions
|
||||||
|
|||||||
+29
-50
@@ -9,7 +9,8 @@ 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.utils.exchange_utils import delete_algo_folder
|
from catalyst.exchange.exchange_utils import delete_algo_folder
|
||||||
|
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
|
||||||
|
|
||||||
@@ -193,7 +194,9 @@ def ipython_only(option):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
help='The name of the targeted exchange.',
|
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
||||||
|
help='The name of the targeted exchange (supported: bitfinex,'
|
||||||
|
' bittrex, poloniex).',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-n',
|
'-n',
|
||||||
@@ -255,9 +258,8 @@ def run(ctx,
|
|||||||
ctx.fail("must specify a base currency with '-c' in backtest mode")
|
ctx.fail("must specify a base currency with '-c' in backtest mode")
|
||||||
|
|
||||||
if capital_base is None:
|
if capital_base is None:
|
||||||
ctx.fail("must specify a capital base with '--capital-base'")
|
ctx.fail("must specify a capital base with '--capital-base'"
|
||||||
|
" in backtest mode")
|
||||||
click.echo('Running in backtesting mode.')
|
|
||||||
|
|
||||||
perf = _run(
|
perf = _run(
|
||||||
initialize=None,
|
initialize=None,
|
||||||
@@ -282,9 +284,7 @@ def run(ctx,
|
|||||||
exchange=exchange_name,
|
exchange=exchange_name,
|
||||||
algo_namespace=algo_namespace,
|
algo_namespace=algo_namespace,
|
||||||
base_currency=base_currency,
|
base_currency=base_currency,
|
||||||
live_graph=False,
|
live_graph=False
|
||||||
simulate_orders=True,
|
|
||||||
stats_output=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if output == '-':
|
if output == '-':
|
||||||
@@ -312,11 +312,11 @@ def catalyst_magic(line, cell=None):
|
|||||||
'--algotext', cell,
|
'--algotext', cell,
|
||||||
'--output', os.devnull, # don't write the results by default
|
'--output', os.devnull, # don't write the results by default
|
||||||
] + ([
|
] + ([
|
||||||
# these options are set when running in line magic mode
|
# these options are set when running in line magic mode
|
||||||
# set a non None algo text to use the ipython user_ns
|
# set a non None algo text to use the ipython user_ns
|
||||||
'--algotext', '',
|
'--algotext', '',
|
||||||
'--local-namespace',
|
'--local-namespace',
|
||||||
] if cell is None else []) + line.split(),
|
] if cell is None else []) + line.split(),
|
||||||
'%s%%catalyst' % ((cell or '') and '%'),
|
'%s%%catalyst' % ((cell or '') and '%'),
|
||||||
# don't use system exit and propogate errors to the caller
|
# don't use system exit and propogate errors to the caller
|
||||||
standalone_mode=False,
|
standalone_mode=False,
|
||||||
@@ -336,12 +336,6 @@ def catalyst_magic(line, cell=None):
|
|||||||
type=click.File('r'),
|
type=click.File('r'),
|
||||||
help='The file that contains the algorithm to run.',
|
help='The file that contains the algorithm to run.',
|
||||||
)
|
)
|
||||||
@click.option(
|
|
||||||
'--capital-base',
|
|
||||||
type=float,
|
|
||||||
show_default=True,
|
|
||||||
help='The amount of capital (in base_currency) allocated to trading.',
|
|
||||||
)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
'-t',
|
'-t',
|
||||||
'--algotext',
|
'--algotext',
|
||||||
@@ -380,7 +374,9 @@ def catalyst_magic(line, cell=None):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
help='The name of the targeted exchange.',
|
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
||||||
|
help='The name of the targeted exchange (supported: bitfinex,'
|
||||||
|
' bittrex, poloniex).',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-n',
|
'-n',
|
||||||
@@ -399,17 +395,9 @@ def catalyst_magic(line, cell=None):
|
|||||||
default=False,
|
default=False,
|
||||||
help='Display live graph.',
|
help='Display live graph.',
|
||||||
)
|
)
|
||||||
@click.option(
|
|
||||||
'--simulate-orders/--no-simulate-orders',
|
|
||||||
is_flag=True,
|
|
||||||
default=True,
|
|
||||||
help='Simulating orders enable the paper trading mode. No orders will be '
|
|
||||||
'sent to the exchange unless set to false.',
|
|
||||||
)
|
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def live(ctx,
|
def live(ctx,
|
||||||
algofile,
|
algofile,
|
||||||
capital_base,
|
|
||||||
algotext,
|
algotext,
|
||||||
define,
|
define,
|
||||||
output,
|
output,
|
||||||
@@ -418,8 +406,7 @@ def live(ctx,
|
|||||||
exchange_name,
|
exchange_name,
|
||||||
algo_namespace,
|
algo_namespace,
|
||||||
base_currency,
|
base_currency,
|
||||||
live_graph,
|
live_graph):
|
||||||
simulate_orders):
|
|
||||||
"""Trade live with the given algorithm.
|
"""Trade live with the given algorithm.
|
||||||
"""
|
"""
|
||||||
if (algotext is not None) == (algofile is not None):
|
if (algotext is not None) == (algofile is not None):
|
||||||
@@ -430,22 +417,11 @@ def live(ctx,
|
|||||||
|
|
||||||
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 algo_namespace is None:
|
if algo_namespace is None:
|
||||||
ctx.fail("must specify an algorithm name '-n' in live execution mode")
|
ctx.fail("must specify an algorithm name '-n' in live execution mode")
|
||||||
|
|
||||||
if base_currency is None:
|
if base_currency is None:
|
||||||
ctx.fail("must specify a base currency '-c' in live execution mode")
|
ctx.fail("must specify a base currency '-c' in live execution mode")
|
||||||
|
|
||||||
if capital_base is None:
|
|
||||||
ctx.fail("must specify a capital base with '--capital-base'")
|
|
||||||
|
|
||||||
if simulate_orders:
|
|
||||||
click.echo('Running in paper trading mode.')
|
|
||||||
|
|
||||||
else:
|
|
||||||
click.echo('Running in live trading mode.')
|
|
||||||
|
|
||||||
perf = _run(
|
perf = _run(
|
||||||
initialize=None,
|
initialize=None,
|
||||||
handle_data=None,
|
handle_data=None,
|
||||||
@@ -455,7 +431,7 @@ def live(ctx,
|
|||||||
algotext=algotext,
|
algotext=algotext,
|
||||||
defines=define,
|
defines=define,
|
||||||
data_frequency=None,
|
data_frequency=None,
|
||||||
capital_base=capital_base,
|
capital_base=None,
|
||||||
data=None,
|
data=None,
|
||||||
bundle=None,
|
bundle=None,
|
||||||
bundle_timestamp=None,
|
bundle_timestamp=None,
|
||||||
@@ -469,9 +445,7 @@ def live(ctx,
|
|||||||
exchange=exchange_name,
|
exchange=exchange_name,
|
||||||
algo_namespace=algo_namespace,
|
algo_namespace=algo_namespace,
|
||||||
base_currency=base_currency,
|
base_currency=base_currency,
|
||||||
live_graph=live_graph,
|
live_graph=live_graph
|
||||||
simulate_orders=simulate_orders,
|
|
||||||
stats_output=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if output == '-':
|
if output == '-':
|
||||||
@@ -486,7 +460,9 @@ def live(ctx,
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
help='The name of the exchange bundle to ingest.',
|
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
||||||
|
help='The name of the exchange bundle to ingest (supported: bitfinex,'
|
||||||
|
' bittrex, poloniex).',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-f',
|
'-f',
|
||||||
@@ -544,8 +520,7 @@ def live(ctx,
|
|||||||
default=False,
|
default=False,
|
||||||
help='Report potential anomalies found in data bundles.'
|
help='Report potential anomalies found in data bundles.'
|
||||||
)
|
)
|
||||||
@click.pass_context
|
def ingest_exchange(exchange_name, data_frequency, start, end,
|
||||||
def ingest_exchange(ctx, exchange_name, data_frequency, start, end,
|
|
||||||
include_symbols, exclude_symbols, csv, show_progress,
|
include_symbols, exclude_symbols, csv, show_progress,
|
||||||
verbose, validate):
|
verbose, validate):
|
||||||
"""
|
"""
|
||||||
@@ -590,7 +565,9 @@ def clean_algo(ctx, algo_namespace):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
help='The name of the exchange bundle to ingest.',
|
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
||||||
|
help='The name of the exchange bundle to ingest (supported: bitfinex,'
|
||||||
|
' bittrex, poloniex).',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-f',
|
'-f',
|
||||||
@@ -629,7 +606,9 @@ def clean_exchange(ctx, exchange_name, data_frequency):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
help='The name of the exchange bundle to ingest.',
|
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
||||||
|
help='The name of the exchange bundle to ingest (supported: bitfinex,'
|
||||||
|
' bittrex, poloniex).',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-c',
|
'-c',
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ from catalyst.utils.events import (
|
|||||||
from catalyst.utils.factory import create_simulation_parameters
|
from catalyst.utils.factory import create_simulation_parameters
|
||||||
from catalyst.utils.math_utils import (
|
from catalyst.utils.math_utils import (
|
||||||
tolerant_equals,
|
tolerant_equals,
|
||||||
|
round_if_near_integer,
|
||||||
round_nearest
|
round_nearest
|
||||||
)
|
)
|
||||||
from catalyst.utils.pandas_utils import clear_dataframe_indexer_caches
|
from catalyst.utils.pandas_utils import clear_dataframe_indexer_caches
|
||||||
@@ -1484,6 +1485,7 @@ class TradingAlgorithm(object):
|
|||||||
"""
|
"""
|
||||||
Converts the number of shares to the smallest tradable lot size for
|
Converts the number of shares to the smallest tradable lot size for
|
||||||
the asset being ordered.
|
the asset being ordered.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return round_nearest(amount, asset.min_trade_size)
|
return round_nearest(amount, asset.min_trade_size)
|
||||||
|
|
||||||
@@ -1521,7 +1523,6 @@ class TradingAlgorithm(object):
|
|||||||
self.updated_portfolio(),
|
self.updated_portfolio(),
|
||||||
self.get_datetime(),
|
self.get_datetime(),
|
||||||
self.trading_client.current_data)
|
self.trading_client.current_data)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __convert_order_params_for_blotter(limit_price, stop_price, style):
|
def __convert_order_params_for_blotter(limit_price, stop_price, style):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
"""
|
"""
|
||||||
Cythonized Asset object.
|
Cythonized Asset object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
cimport cython
|
cimport cython
|
||||||
@@ -39,7 +38,7 @@ from numpy cimport int64_t
|
|||||||
import warnings
|
import warnings
|
||||||
cimport numpy as np
|
cimport numpy as np
|
||||||
|
|
||||||
from catalyst.exchange.utils.exchange_utils import get_sid
|
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
|
||||||
|
|
||||||
@@ -406,9 +405,6 @@ cdef class TradingPair(Asset):
|
|||||||
cdef readonly float taker
|
cdef readonly float taker
|
||||||
cdef readonly int trading_state
|
cdef readonly int trading_state
|
||||||
cdef readonly object data_source
|
cdef readonly object data_source
|
||||||
cdef readonly float max_trade_size
|
|
||||||
cdef readonly float lot
|
|
||||||
cdef readonly int decimals
|
|
||||||
|
|
||||||
_kwargnames = frozenset({
|
_kwargnames = frozenset({
|
||||||
'sid',
|
'sid',
|
||||||
@@ -427,13 +423,10 @@ cdef class TradingPair(Asset):
|
|||||||
'end_minute',
|
'end_minute',
|
||||||
'exchange_symbol',
|
'exchange_symbol',
|
||||||
'min_trade_size',
|
'min_trade_size',
|
||||||
'max_trade_size',
|
|
||||||
'lot',
|
|
||||||
'maker',
|
'maker',
|
||||||
'taker',
|
'taker',
|
||||||
'trading_state',
|
'trading_state',
|
||||||
'data_source',
|
'data_source'
|
||||||
'decimals'
|
|
||||||
})
|
})
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
object symbol,
|
object symbol,
|
||||||
@@ -450,11 +443,8 @@ cdef class TradingPair(Asset):
|
|||||||
object auto_close_date=None,
|
object auto_close_date=None,
|
||||||
object exchange_full=None,
|
object exchange_full=None,
|
||||||
float min_trade_size=0.0001,
|
float min_trade_size=0.0001,
|
||||||
float max_trade_size=1000000,
|
|
||||||
float maker=0.0015,
|
float maker=0.0015,
|
||||||
float taker=0.0025,
|
float taker=0.0025,
|
||||||
float lot=0,
|
|
||||||
int decimals = 8,
|
|
||||||
int trading_state=0,
|
int trading_state=0,
|
||||||
object data_source='catalyst'):
|
object data_source='catalyst'):
|
||||||
"""
|
"""
|
||||||
@@ -519,12 +509,9 @@ cdef class TradingPair(Asset):
|
|||||||
:param auto_close_date:
|
:param auto_close_date:
|
||||||
:param exchange_full:
|
:param exchange_full:
|
||||||
:param min_trade_size:
|
:param min_trade_size:
|
||||||
:param max_trade_size:
|
|
||||||
:param maker:
|
:param maker:
|
||||||
:param taker:
|
:param taker:
|
||||||
:param data_source
|
:param data_source
|
||||||
:param decimals
|
|
||||||
:param lot
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
symbol = symbol.lower()
|
symbol = symbol.lower()
|
||||||
@@ -548,9 +535,6 @@ cdef class TradingPair(Asset):
|
|||||||
if end_date is None:
|
if end_date is None:
|
||||||
end_date = pd.Timestamp.utcnow() + timedelta(days=365)
|
end_date = pd.Timestamp.utcnow() + timedelta(days=365)
|
||||||
|
|
||||||
if lot == 0 and min_trade_size > 0:
|
|
||||||
lot = min_trade_size
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
sid,
|
sid,
|
||||||
exchange,
|
exchange,
|
||||||
@@ -572,9 +556,6 @@ cdef class TradingPair(Asset):
|
|||||||
self.exchange_symbol = exchange_symbol
|
self.exchange_symbol = exchange_symbol
|
||||||
self.trading_state = trading_state
|
self.trading_state = trading_state
|
||||||
self.data_source = data_source
|
self.data_source = data_source
|
||||||
self.max_trade_size = max_trade_size
|
|
||||||
self.lot = lot
|
|
||||||
self.decimals = decimals
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Trading Pair {symbol}({sid}) Exchange: {exchange}, ' \
|
return 'Trading Pair {symbol}({sid}) Exchange: {exchange}, ' \
|
||||||
@@ -601,7 +582,6 @@ cdef class TradingPair(Asset):
|
|||||||
"""
|
"""
|
||||||
Convert to a python dict.
|
Convert to a python dict.
|
||||||
"""
|
"""
|
||||||
#TODO: missing fields
|
|
||||||
super_dict = super(TradingPair, self).to_dict()
|
super_dict = super(TradingPair, self).to_dict()
|
||||||
super_dict['end_daily'] = self.end_daily
|
super_dict['end_daily'] = self.end_daily
|
||||||
super_dict['end_minute'] = self.end_minute
|
super_dict['end_minute'] = self.end_minute
|
||||||
@@ -630,7 +610,6 @@ cdef class TradingPair(Asset):
|
|||||||
and whose second element is a tuple of all the attributes that should
|
and whose second element is a tuple of all the attributes that should
|
||||||
be serialized/deserialized during pickling.
|
be serialized/deserialized during pickling.
|
||||||
"""
|
"""
|
||||||
#TODO: make sure that all fields set there
|
|
||||||
return (self.__class__, (self.symbol,
|
return (self.__class__, (self.symbol,
|
||||||
self.exchange,
|
self.exchange,
|
||||||
self.start_date,
|
self.start_date,
|
||||||
@@ -641,12 +620,7 @@ cdef class TradingPair(Asset):
|
|||||||
self.first_traded,
|
self.first_traded,
|
||||||
self.auto_close_date,
|
self.auto_close_date,
|
||||||
self.exchange_full,
|
self.exchange_full,
|
||||||
self.min_trade_size,
|
self.min_trade_size))
|
||||||
self.max_trade_size,
|
|
||||||
self.lot,
|
|
||||||
self.decimals,
|
|
||||||
self.taker,
|
|
||||||
self.maker))
|
|
||||||
|
|
||||||
def make_asset_array(int size, Asset asset):
|
def make_asset_array(int size, Asset asset):
|
||||||
cdef np.ndarray out = np.empty([size], dtype=object)
|
cdef np.ndarray out = np.empty([size], dtype=object)
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import logbook
|
|||||||
For example, if you want to see the DEBUG messages, run:
|
For example, if you want to see the DEBUG messages, run:
|
||||||
$ export CATALYST_LOG_LEVEL=10
|
$ export CATALYST_LOG_LEVEL=10
|
||||||
'''
|
'''
|
||||||
LOG_LEVEL = int(os.environ.get('CATALYST_LOG_LEVEL', logbook.INFO))
|
# LOG_LEVEL = int(os.environ.get('CATALYST_LOG_LEVEL', logbook.INFO))
|
||||||
|
LOG_LEVEL = logbook.DEBUG
|
||||||
|
|
||||||
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
|
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
|
||||||
'{exchange}/symbols.json'
|
'{exchange}/symbols.json'
|
||||||
|
|||||||
+104
-110
@@ -1,33 +1,25 @@
|
|||||||
import csv
|
import json, time, csv
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import logbook
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import requests
|
import os, time, shutil, requests, logbook
|
||||||
|
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename
|
||||||
|
|
||||||
from catalyst.exchange.utils.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 = pd.to_datetime('today').value // 10 ** 9
|
DT_END = pd.to_datetime('today').value // 10 ** 9
|
||||||
CSV_OUT_FOLDER = os.environ.get('CSV_OUT_FOLDER', '/efs/exchanges/poloniex/')
|
CSV_OUT_FOLDER = os.environ.get('CSV_OUT_FOLDER', '/efs/exchanges/poloniex/')
|
||||||
CONN_RETRIES = 2
|
CONN_RETRIES = 2
|
||||||
|
|
||||||
logbook.StderrHandler().push_application()
|
logbook.StderrHandler().push_application()
|
||||||
log = logbook.Logger(__name__)
|
log = logbook.Logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PoloniexCurator(object):
|
class PoloniexCurator(object):
|
||||||
'''
|
'''
|
||||||
OHLCV data feed generator for crypto data. Based on Poloniex market data
|
OHLCV data feed generator for crypto data. Based on Poloniex market data
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_api_path = 'https://poloniex.com/public?'
|
_api_path = 'https://poloniex.com/public?'
|
||||||
currency_pairs = []
|
currency_pairs = []
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if not os.path.exists(CSV_OUT_FOLDER):
|
if not os.path.exists(CSV_OUT_FOLDER):
|
||||||
@@ -38,6 +30,7 @@ class PoloniexCurator(object):
|
|||||||
CSV_OUT_FOLDER))
|
CSV_OUT_FOLDER))
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
|
|
||||||
def get_currency_pairs(self):
|
def get_currency_pairs(self):
|
||||||
'''
|
'''
|
||||||
Retrieves and returns all currency pairs from the exchange
|
Retrieves and returns all currency pairs from the exchange
|
||||||
@@ -52,7 +45,7 @@ class PoloniexCurator(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
self.currency_pairs = []
|
self.currency_pairs = []
|
||||||
for ticker in data:
|
for ticker in data:
|
||||||
self.currency_pairs.append(ticker)
|
self.currency_pairs.append(ticker)
|
||||||
self.currency_pairs.sort()
|
self.currency_pairs.sort()
|
||||||
@@ -61,15 +54,18 @@ class PoloniexCurator(object):
|
|||||||
len(self.currency_pairs)
|
len(self.currency_pairs)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _retrieve_tradeID_date(self, row):
|
def _retrieve_tradeID_date(self, row):
|
||||||
'''
|
'''
|
||||||
Helper function that reads tradeID and date fields from CSV readline
|
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],
|
d = pd.to_datetime(row.split(',')[1],
|
||||||
infer_datetime_format=True).value // 10 ** 9
|
infer_datetime_format=True).value // 10 ** 9
|
||||||
return tId, d
|
return tId, d
|
||||||
|
|
||||||
|
|
||||||
def retrieve_trade_history(self, currencyPair, start=DT_START,
|
def retrieve_trade_history(self, currencyPair, start=DT_START,
|
||||||
end=DT_END, temp=None):
|
end=DT_END, temp=None):
|
||||||
'''
|
'''
|
||||||
@@ -94,27 +90,18 @@ class PoloniexCurator(object):
|
|||||||
f.seek(0, os.SEEK_END)
|
f.seek(0, os.SEEK_END)
|
||||||
if(f.tell() > 2): # Check file size is not 0
|
if(f.tell() > 2): # Check file size is not 0
|
||||||
f.seek(0) # Go to start to read
|
f.seek(0) # Go to start to read
|
||||||
last_tradeID, end_file = self._retrieve_tradeID_date(
|
last_tradeID, end_file = self._retrieve_tradeID_date(f.readline())
|
||||||
f.readline())
|
|
||||||
f.seek(-2, os.SEEK_END) # Jump to the 2nd 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...
|
||||||
# ...jump back the read byte plus one more.
|
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
|
||||||
f.seek(-2, os.SEEK_CUR)
|
first_tradeID, start_file = self._retrieve_tradeID_date(f.readline())
|
||||||
first_tradeID, start_file = self._retrieve_tradeID_date(
|
|
||||||
f.readline())
|
|
||||||
|
|
||||||
if(end_file + 3600 * 6 > DT_END
|
if( end_file + 3600 * 6 > DT_END and ( first_tradeID == 1
|
||||||
and (first_tradeID == 1
|
or (currencyPair == 'BTC_HUC' and first_tradeID == 2)
|
||||||
or (currencyPair == 'BTC_HUC'
|
or (currencyPair == 'BTC_RIC' and first_tradeID == 2)
|
||||||
and first_tradeID == 2)
|
or (currencyPair == 'BTC_XCP' and first_tradeID == 2)
|
||||||
or (currencyPair == 'BTC_RIC'
|
or (currencyPair == 'BTC_NAV' and first_tradeID == 4569)
|
||||||
and first_tradeID == 2)
|
or (currencyPair == 'BTC_POT' and first_tradeID == 23511) ) ):
|
||||||
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:
|
||||||
@@ -126,7 +113,7 @@ class PoloniexCurator(object):
|
|||||||
than 1 month, so we make sure that start date is never more than
|
than 1 month, so we make sure that start date is never more than
|
||||||
1 month apart from end date
|
1 month apart from end date
|
||||||
'''
|
'''
|
||||||
if(end - start > 2419200): # 60s/min * 60min/hr * 24hr/day * 28days
|
if( end - start > 2419200 ): # 60s/min * 60min/hr * 24hr/day * 28days
|
||||||
newstart = end - 2419200
|
newstart = end - 2419200
|
||||||
else:
|
else:
|
||||||
newstart = start
|
newstart = start
|
||||||
@@ -137,11 +124,12 @@ class PoloniexCurator(object):
|
|||||||
|
|
||||||
url = '{path}command=returnTradeHistory¤cyPair={pair}' \
|
url = '{path}command=returnTradeHistory¤cyPair={pair}' \
|
||||||
'&start={start}&end={end}'.format(
|
'&start={start}&end={end}'.format(
|
||||||
path=self._api_path,
|
path = self._api_path,
|
||||||
pair=currencyPair,
|
pair = currencyPair,
|
||||||
start=str(newstart),
|
start = str(newstart),
|
||||||
end=str(end)
|
end = str(end)
|
||||||
)
|
)
|
||||||
|
print url
|
||||||
|
|
||||||
attempts = 0
|
attempts = 0
|
||||||
success = 0
|
success = 0
|
||||||
@@ -149,14 +137,14 @@ class PoloniexCurator(object):
|
|||||||
try:
|
try:
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Failed to retrieve trade history data'
|
log.error('Failed to retrieve trade history data for {}'.format(
|
||||||
'for {}'.format(currencyPair))
|
currencyPair
|
||||||
|
))
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
attempts += 1
|
attempts += 1
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if(isinstance(response.json(), dict)
|
if isinstance(response.json(), dict) and response.json()['error']:
|
||||||
and response.json()['error']):
|
|
||||||
log.error('Failed to to retrieve trade history data '
|
log.error('Failed to to retrieve trade history data '
|
||||||
'for {}: {}'.format(
|
'for {}: {}'.format(
|
||||||
currencyPair,
|
currencyPair,
|
||||||
@@ -173,6 +161,7 @@ class PoloniexCurator(object):
|
|||||||
if not success:
|
if not success:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
If we get to transactionId == 1, and we already have that on
|
If we get to transactionId == 1, and we already have that on
|
||||||
disk, we got to the end of TradeHistory for this coin.
|
disk, we got to the end of TradeHistory for this coin.
|
||||||
@@ -193,13 +182,12 @@ class PoloniexCurator(object):
|
|||||||
for this currencyPair
|
for this currencyPair
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
if(temp is not None
|
if( 'end_file' in locals() and end_file + 3600 < end):
|
||||||
or ('end_file' in locals() and end_file + 3600 < end)):
|
|
||||||
if (temp is None):
|
if (temp is None):
|
||||||
temp = os.tmpfile()
|
temp = os.tmpfile()
|
||||||
tempcsv = csv.writer(temp)
|
tempcsv = csv.writer(temp)
|
||||||
for item in response.json():
|
for item in response.json():
|
||||||
if(item['tradeID'] <= last_tradeID):
|
if( item['tradeID'] <= last_tradeID ):
|
||||||
continue
|
continue
|
||||||
tempcsv.writerow([
|
tempcsv.writerow([
|
||||||
item['tradeID'],
|
item['tradeID'],
|
||||||
@@ -208,28 +196,27 @@ class PoloniexCurator(object):
|
|||||||
item['rate'],
|
item['rate'],
|
||||||
item['amount'],
|
item['amount'],
|
||||||
item['total'],
|
item['total'],
|
||||||
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'],
|
end = pd.to_datetime( response.json()[-1]['date'],
|
||||||
infer_datetime_format=True
|
infer_datetime_format=True).value // 10 ** 9
|
||||||
).value // 10**9
|
|
||||||
self.retrieve_trade_history(currencyPair, start,
|
self.retrieve_trade_history(currencyPair, start,
|
||||||
end, temp=temp)
|
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)
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
temp.seek(0)
|
temp.seek(0)
|
||||||
shutil.copyfileobj(temp, f)
|
shutil.copyfileobj(temp,f)
|
||||||
temp.close()
|
temp.close()
|
||||||
end = start_file
|
end = start_file
|
||||||
else:
|
else:
|
||||||
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()
|
if( 'first_tradeID' in locals()
|
||||||
and item['tradeID'] >= first_tradeID):
|
and item['tradeID'] >= first_tradeID ):
|
||||||
continue
|
continue
|
||||||
csvwriter.writerow([
|
csvwriter.writerow([
|
||||||
item['tradeID'],
|
item['tradeID'],
|
||||||
@@ -241,7 +228,7 @@ class PoloniexCurator(object):
|
|||||||
item['globalTradeID']
|
item['globalTradeID']
|
||||||
])
|
])
|
||||||
end = pd.to_datetime(response.json()[-1]['date'],
|
end = pd.to_datetime(response.json()[-1]['date'],
|
||||||
infer_datetime_format=True).value//10**9
|
infer_datetime_format=True).value // 10 ** 9
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Error opening {}'.format(csv_fn))
|
log.error('Error opening {}'.format(csv_fn))
|
||||||
@@ -253,49 +240,53 @@ class PoloniexCurator(object):
|
|||||||
'''
|
'''
|
||||||
self.retrieve_trade_history(currencyPair, start, end)
|
self.retrieve_trade_history(currencyPair, start, end)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def generate_ohlcv(self, df):
|
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
|
||||||
'''
|
'''
|
||||||
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') # set Vol aside
|
||||||
df.drop('total', axis=1, inplace=True) # Drop volume data
|
df.drop('total', axis=1, inplace=True) # Drop volume data
|
||||||
ohlc = df.resample('T').ohlc() # Resample OHLC 1min
|
ohlc = df.resample('T').ohlc() # Resample OHLC 1min
|
||||||
ohlc.columns = ohlc.columns.map(lambda t: t[1]) # Rename cols
|
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 fwd missing 'close'
|
||||||
ohlc = ohlc.apply(lambda x: x.fillna(closes)) # Fill NA w/ 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) # Concat OHLC + Vol
|
ohlcv = pd.concat([ohlc,vol], axis=1) # Concatenate OHLC + Vol
|
||||||
return ohlcv
|
return ohlcv
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def write_ohlcv_file(self, currencyPair):
|
def write_ohlcv_file(self, currencyPair):
|
||||||
'''
|
'''
|
||||||
Generates OHLCV data file with 1minute bars from TradeHistory on disk
|
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.getmtime(csv_1min) > time.time() - 7200):
|
if( os.path.getmtime(csv_1min) > time.time() - 7200 ):
|
||||||
log.debug(currencyPair+': 1min data file already up to date. '
|
log.debug(currencyPair+': 1min data file already up to date. '
|
||||||
'Delete the file if you want to rebuild it.')
|
'Delete the file if you want to rebuild it.')
|
||||||
else:
|
else:
|
||||||
df = pd.read_csv(csv_trades,
|
df = pd.read_csv(csv_trades,
|
||||||
names=['tradeID',
|
names=['tradeID',
|
||||||
'date',
|
'date',
|
||||||
'type',
|
'type',
|
||||||
'rate',
|
'rate',
|
||||||
'amount',
|
'amount',
|
||||||
'total',
|
'total',
|
||||||
'globalTradeID'],
|
'globalTradeID'],
|
||||||
dtype={'tradeID': int,
|
dtype = {'tradeID': int,
|
||||||
'date': str,
|
'date': str,
|
||||||
'type': str,
|
'type': str,
|
||||||
'rate': float,
|
'rate': float,
|
||||||
'amount': float,
|
'amount': float,
|
||||||
'total': float,
|
'total': float,
|
||||||
'globalTradeID': int}
|
'globalTradeID': int }
|
||||||
)
|
)
|
||||||
df.drop(['tradeID', 'type', 'amount', 'globalTradeID'],
|
df.drop(['tradeID','type','amount','globalTradeID'],
|
||||||
axis=1, inplace=True)
|
axis=1, inplace=True)
|
||||||
df['date'] = pd.to_datetime(df['date'], infer_datetime_format=True)
|
df['date'] = pd.to_datetime(df['date'], infer_datetime_format=True)
|
||||||
ohlcv = self.generate_ohlcv(df)
|
ohlcv = self.generate_ohlcv(df)
|
||||||
@@ -314,24 +305,28 @@ class PoloniexCurator(object):
|
|||||||
item.volume,
|
item.volume,
|
||||||
])
|
])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Error opening {}'.format(csv_1min))
|
log.error('Error opening {}'.format(csv_fn))
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
log.debug('{}: Generated 1min OHLCV data.'.format(currencyPair))
|
log.debug('{}: Generated 1min OHLCV data.'.format(currencyPair))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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
|
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',
|
df = pd.read_csv(csv_fn, names=['date',
|
||||||
'open',
|
'open',
|
||||||
'high',
|
'high',
|
||||||
'low',
|
'low',
|
||||||
'close',
|
'close',
|
||||||
'volume'])
|
'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]
|
||||||
|
|
||||||
|
|
||||||
def generate_symbols_json(self, filename=None):
|
def generate_symbols_json(self, filename=None):
|
||||||
'''
|
'''
|
||||||
@@ -346,37 +341,36 @@ 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 = '{}crypto_trades-{}.csv'.format(
|
csv_fn = '{}crypto_trades-{}.csv'.format(
|
||||||
CSV_OUT_FOLDER,
|
CSV_OUT_FOLDER, currencyPair)
|
||||||
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): # Check file size is not 0
|
if(f.tell() > 2): # Check file size is not 0
|
||||||
f.seek(-2, os.SEEK_END) # Jump to 2nd 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...
|
||||||
# ...jump back the read byte plus one more.
|
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
|
||||||
f.seek(-2, os.SEEK_CUR)
|
start = pd.to_datetime( f.readline().split(',')[1],
|
||||||
start = pd.to_datetime(f.readline().split(',')[1],
|
infer_datetime_format=True)
|
||||||
infer_datetime_format=True)
|
|
||||||
|
|
||||||
if(start is None):
|
if(start is None):
|
||||||
start = time.gmtime()
|
start = time.gmtime()
|
||||||
base, market = currencyPair.lower().split('_')
|
base, market = currencyPair.lower().split('_')
|
||||||
symbol = '{market}_{base}'.format(market=market, base=base)
|
symbol = '{market}_{base}'.format( market=market, base=base )
|
||||||
symbol_map[currencyPair] = dict(
|
symbol_map[currencyPair] = dict(
|
||||||
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,
|
json.dump(symbol_map, symbols, sort_keys=True, indent=2,
|
||||||
separators=(',', ':'))
|
separators=(',',':'))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
pc = PoloniexCurator()
|
pc = PoloniexCurator()
|
||||||
pc.get_currency_pairs()
|
pc.get_currency_pairs()
|
||||||
# pc.generate_symbols_json()
|
#pc.generate_symbols_json()
|
||||||
|
|
||||||
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))
|
log.debug('{} up to date.'.format(currencyPair))
|
||||||
pc.write_ohlcv_file(currencyPair)
|
pc.write_ohlcv_file(currencyPair)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# These imports are necessary to force module-scope register calls to happen.
|
# These imports are necessary to force module-scope register calls to happen.
|
||||||
from . import quandl # noqa
|
from . import quandl # noqa
|
||||||
|
from . import poloniex
|
||||||
from .core import (
|
from .core import (
|
||||||
UnknownBundle,
|
UnknownBundle,
|
||||||
bundles,
|
bundles,
|
||||||
|
|||||||
@@ -13,9 +13,10 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
|
|
||||||
from itertools import count
|
from itertools import count
|
||||||
import tarfile
|
import tarfile
|
||||||
from time import sleep
|
from time import time, sleep
|
||||||
|
|
||||||
from abc import abstractmethod, abstractproperty
|
from abc import abstractmethod, abstractproperty
|
||||||
import logbook
|
import logbook
|
||||||
@@ -36,7 +37,6 @@ log = logbook.Logger(__name__, level=LOG_LEVEL)
|
|||||||
|
|
||||||
DEFAULT_RETRIES = 5
|
DEFAULT_RETRIES = 5
|
||||||
|
|
||||||
|
|
||||||
class BaseBundle(object):
|
class BaseBundle(object):
|
||||||
def __init__(self, asset_filter=[]):
|
def __init__(self, asset_filter=[]):
|
||||||
self._asset_filter = asset_filter
|
self._asset_filter = asset_filter
|
||||||
@@ -128,7 +128,7 @@ class BaseBundle(object):
|
|||||||
retries = environ.get('CATALYST_DOWNLOAD_ATTEMPTS', 5)
|
retries = environ.get('CATALYST_DOWNLOAD_ATTEMPTS', 5)
|
||||||
|
|
||||||
if is_compile:
|
if is_compile:
|
||||||
# User has instructed local compilation & ingestion of bundle.
|
# User has instructed local compilation and ingestion of bundle.
|
||||||
# Fetch raw metadata for all symbols.
|
# Fetch raw metadata for all symbols.
|
||||||
raw_metadata = self._fetch_metadata_frame(
|
raw_metadata = self._fetch_metadata_frame(
|
||||||
api_key,
|
api_key,
|
||||||
@@ -157,9 +157,9 @@ class BaseBundle(object):
|
|||||||
show_progress=show_progress,
|
show_progress=show_progress,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Post-process metadata using cached symbol frames, and write
|
# Post-process metadata using cached symbol frames, and write to
|
||||||
# to disk. This metadata must be written before any attempt
|
# disk. This metadata must be written before any attempt to write
|
||||||
# to write minute data.
|
# minute data.
|
||||||
metadata = self._post_process_metadata(
|
metadata = self._post_process_metadata(
|
||||||
raw_metadata,
|
raw_metadata,
|
||||||
cache,
|
cache,
|
||||||
@@ -184,11 +184,10 @@ class BaseBundle(object):
|
|||||||
show_progress=show_progress,
|
show_progress=show_progress,
|
||||||
)
|
)
|
||||||
|
|
||||||
# For legacy purposes, this call is required to ensure the
|
# For legacy purposes, this call is required to ensure the database
|
||||||
# database contains an appropriately initialized file
|
# contains an appropriately initialized file structure. We don't
|
||||||
# structure. We don't forsee a usecase for adjustments at
|
# forsee a usecase for adjustments at this time, but may later
|
||||||
# this time, but may later choose to expose this functionality
|
# choose to expose this functionality in the future.
|
||||||
# in the future.
|
|
||||||
adjustment_writer.write(
|
adjustment_writer.write(
|
||||||
splits=(
|
splits=(
|
||||||
pd.concat(self.splits, ignore_index=True)
|
pd.concat(self.splits, ignore_index=True)
|
||||||
@@ -233,11 +232,11 @@ class BaseBundle(object):
|
|||||||
tar.extractall(output_dir)
|
tar.extractall(output_dir)
|
||||||
|
|
||||||
def _fetch_metadata_frame(self,
|
def _fetch_metadata_frame(self,
|
||||||
api_key,
|
api_key,
|
||||||
cache,
|
cache,
|
||||||
retries=DEFAULT_RETRIES,
|
retries=DEFAULT_RETRIES,
|
||||||
environ=None,
|
environ=None,
|
||||||
show_progress=False):
|
show_progress=False):
|
||||||
|
|
||||||
# Setup raw metadata iterator to fetch pages if necessary.
|
# Setup raw metadata iterator to fetch pages if necessary.
|
||||||
raw_iter = self._fetch_metadata_iter(api_key, cache, retries, environ)
|
raw_iter = self._fetch_metadata_iter(api_key, cache, retries, environ)
|
||||||
@@ -270,10 +269,10 @@ class BaseBundle(object):
|
|||||||
page_number,
|
page_number,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
raw = pd.DataFrame([])
|
raw = pd.DataFrame([])
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception as e:
|
||||||
log.exception(
|
log.exception(
|
||||||
'Failed to load metadata from {}. '
|
'Failed to load metadata from {}. '
|
||||||
'Retrying.'.format(self.name)
|
'Retrying.'.format(self.name)
|
||||||
@@ -284,6 +283,7 @@ class BaseBundle(object):
|
|||||||
'attempts.'.format(page_number, retries)
|
'attempts.'.format(page_number, retries)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if raw.empty:
|
if raw.empty:
|
||||||
# Empty DataFrame signals completion.
|
# Empty DataFrame signals completion.
|
||||||
break
|
break
|
||||||
@@ -318,16 +318,16 @@ class BaseBundle(object):
|
|||||||
show_percent=False,
|
show_percent=False,
|
||||||
) as symbols_map:
|
) as symbols_map:
|
||||||
for asset_id, symbol in symbols_map:
|
for asset_id, symbol in symbols_map:
|
||||||
# Attempt to load data from disk, the cache should have an
|
# Attempt to load data from disk, the cache should have an entry
|
||||||
# entry for each symbol at this point of the execution. If one
|
# for each symbol at this point of the execution. If one does
|
||||||
# does not exist, we should fail.
|
# not exist, we should fail.
|
||||||
key = '{sym}.daily.frame'.format(sym=symbol)
|
key = '{sym}.daily.frame'.format(sym=symbol)
|
||||||
try:
|
try:
|
||||||
raw_data = cache[key]
|
raw_data = cache[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Unable to find cached data for symbol:'
|
'Unable to find cached data for symbol: {0}'.format(symbol)
|
||||||
' {0}'.format(symbol))
|
)
|
||||||
|
|
||||||
# Perform and require post-processing of metadata.
|
# Perform and require post-processing of metadata.
|
||||||
final_symbol_metadata = self.post_process_symbol_metadata(
|
final_symbol_metadata = self.post_process_symbol_metadata(
|
||||||
@@ -363,8 +363,8 @@ class BaseBundle(object):
|
|||||||
# returns the cached data unaltered. The `should_sleep` flag
|
# returns the cached data unaltered. The `should_sleep` flag
|
||||||
# indicates that an API call was attempted, and that we should be
|
# indicates that an API call was attempted, and that we should be
|
||||||
# ensure aren't exceeding our rate limit before proceeding to the
|
# ensure aren't exceeding our rate limit before proceeding to the
|
||||||
# next symbol. If the raw_data is updated, it is cached before
|
# next symbol. If the raw_data is updated, it is cached before being
|
||||||
# being returned.
|
# returned.
|
||||||
raw_data, should_sleep = self._maybe_update_symbol_frame(
|
raw_data, should_sleep = self._maybe_update_symbol_frame(
|
||||||
start_time,
|
start_time,
|
||||||
api_key,
|
api_key,
|
||||||
@@ -468,6 +468,7 @@ class BaseBundle(object):
|
|||||||
data_frequency,
|
data_frequency,
|
||||||
)
|
)
|
||||||
raw_data.index = pd.to_datetime(raw_data.index, utc=True)
|
raw_data.index = pd.to_datetime(raw_data.index, utc=True)
|
||||||
|
#raw_data.index = raw_data.index.tz_localize('UTC')
|
||||||
|
|
||||||
# Filter incoming data to fit start and end sessions.
|
# Filter incoming data to fit start and end sessions.
|
||||||
raw_data = raw_data[
|
raw_data = raw_data[
|
||||||
@@ -481,7 +482,7 @@ class BaseBundle(object):
|
|||||||
|
|
||||||
return raw_data
|
return raw_data
|
||||||
|
|
||||||
except Exception:
|
except Exception as e:
|
||||||
log.exception(
|
log.exception(
|
||||||
'Exception raised fetching {name} data. Retrying.'
|
'Exception raised fetching {name} data. Retrying.'
|
||||||
.format(name=self.name)
|
.format(name=self.name)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
from catalyst.data.bundles.base import BaseBundle
|
from catalyst.data.bundles.base import BaseBundle
|
||||||
from catalyst.utils.memoize import lazyval
|
from catalyst.utils.memoize import lazyval
|
||||||
|
|
||||||
|
|
||||||
class BasePricingBundle(BaseBundle):
|
class BasePricingBundle(BaseBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def md_dtypes(self):
|
def md_dtypes(self):
|
||||||
@@ -39,7 +38,6 @@ class BasePricingBundle(BaseBundle):
|
|||||||
('volume', 'float64'),
|
('volume', 'float64'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class BaseCryptoPricingBundle(BasePricingBundle):
|
class BaseCryptoPricingBundle(BasePricingBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def calendar_name(self):
|
def calendar_name(self):
|
||||||
@@ -57,7 +55,6 @@ class BaseCryptoPricingBundle(BasePricingBundle):
|
|||||||
def dividends(self):
|
def dividends(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
class BaseEquityPricingBundle(BasePricingBundle):
|
class BaseEquityPricingBundle(BasePricingBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def calendar_name(self):
|
def calendar_name(self):
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ from catalyst.utils.cli import maybe_show_progress
|
|||||||
|
|
||||||
ONE_MEGABYTE = 1024 * 1024
|
ONE_MEGABYTE = 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
def asset_db_path(bundle_name, timestr, environ=None, db_version=None):
|
def asset_db_path(bundle_name, timestr, environ=None, db_version=None):
|
||||||
return pth.data_path(
|
return pth.data_path(
|
||||||
asset_db_relative(bundle_name, timestr, environ, db_version),
|
asset_db_relative(bundle_name, timestr, environ, db_version),
|
||||||
@@ -136,7 +135,6 @@ def ingestions_for_bundle(bundle, environ=None):
|
|||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def download_with_progress(url, chunk_size, **progress_kwargs):
|
def download_with_progress(url, chunk_size, **progress_kwargs):
|
||||||
"""
|
"""
|
||||||
Download streaming data from a URL, printing progress information to the
|
Download streaming data from a URL, printing progress information to the
|
||||||
@@ -707,5 +705,4 @@ def _make_bundle_core():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
bundles, register_bundle, register, unregister, ingest, load, clean = \
|
bundles, register_bundle, register, unregister, ingest, load, clean = _make_bundle_core()
|
||||||
_make_bundle_core()
|
|
||||||
|
|||||||
@@ -14,17 +14,19 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from six.moves.urllib.parse import urlencode
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
from six.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
from catalyst.data.bundles.core import register_bundle
|
from catalyst.data.bundles.core import register_bundle
|
||||||
from catalyst.data.bundles.base_pricing import BaseCryptoPricingBundle
|
from catalyst.data.bundles.base_pricing import BaseCryptoPricingBundle
|
||||||
from catalyst.utils.memoize import lazyval
|
from catalyst.utils.memoize import lazyval
|
||||||
|
|
||||||
from catalyst.curate.poloniex import PoloniexCurator
|
from catalyst.curate.poloniex import PoloniexCurator
|
||||||
|
|
||||||
|
|
||||||
class PoloniexBundle(BaseCryptoPricingBundle):
|
class PoloniexBundle(BaseCryptoPricingBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def name(self):
|
def name(self):
|
||||||
@@ -44,8 +46,7 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
@lazyval
|
@lazyval
|
||||||
def tar_url(self):
|
def tar_url(self):
|
||||||
return (
|
return (
|
||||||
'https://s3.amazonaws.com/enigmaco/catalyst-bundles/'
|
'https://s3.amazonaws.com/enigmaco/catalyst-bundles/poloniex/poloniex-bundle.tar.gz'
|
||||||
'poloniex/poloniex-bundle.tar.gz'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
@@ -66,11 +67,12 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
|
|
||||||
raw = raw.sort_index().reset_index()
|
raw = raw.sort_index().reset_index()
|
||||||
raw.rename(
|
raw.rename(
|
||||||
columns={'index': 'symbol'},
|
columns={'index':'symbol'},
|
||||||
inplace=True,
|
inplace=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
raw = raw[raw['isFrozen'] == 0]
|
raw = raw[raw['isFrozen'] == 0]
|
||||||
|
|
||||||
return raw
|
return raw
|
||||||
|
|
||||||
def post_process_symbol_metadata(self, asset_id, sym_md, sym_data):
|
def post_process_symbol_metadata(self, asset_id, sym_md, sym_data):
|
||||||
@@ -96,8 +98,7 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
frequency):
|
frequency):
|
||||||
|
|
||||||
# TODO: replace this with direct exchange call
|
# TODO: replace this with direct exchange call
|
||||||
# The end date and frequency should be used to
|
# The end date and frequency should be used to calculate the number of bars
|
||||||
# calculate the number of bars
|
|
||||||
if(frequency == 'minute'):
|
if(frequency == 'minute'):
|
||||||
pc = PoloniexCurator()
|
pc = PoloniexCurator()
|
||||||
raw = pc.onemin_to_dataframe(symbol, start_date, end_date)
|
raw = pc.onemin_to_dataframe(symbol, start_date, end_date)
|
||||||
@@ -115,9 +116,8 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
)
|
)
|
||||||
raw.set_index('date', inplace=True)
|
raw.set_index('date', inplace=True)
|
||||||
|
|
||||||
# BcolzDailyBarReader introduces a 1/1000 factor in the way
|
# BcolzDailyBarReader introduces a 1/1000 factor in the way pricing is stored
|
||||||
# pricing is stored on disk, which we compensate here to get
|
# on disk, which we compensate here to get the right pricing amounts
|
||||||
# the right pricing amounts
|
|
||||||
# ref: data/us_equity_pricing.py
|
# ref: data/us_equity_pricing.py
|
||||||
scale = 1
|
scale = 1
|
||||||
raw.loc[:, 'open'] /= scale
|
raw.loc[:, 'open'] /= scale
|
||||||
@@ -139,6 +139,7 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
|
|
||||||
return self._format_polo_query(query_params)
|
return self._format_polo_query(query_params)
|
||||||
|
|
||||||
|
|
||||||
def _format_data_url(self,
|
def _format_data_url(self,
|
||||||
api_key,
|
api_key,
|
||||||
symbol,
|
symbol,
|
||||||
@@ -170,7 +171,6 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
query=urlencode(query_params),
|
query=urlencode(query_params),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
As a second parameter, you can pass an array of currency pairs
|
As a second parameter, you can pass an array of currency pairs
|
||||||
that will be processed as an asset_filter to only process that
|
that will be processed as an asset_filter to only process that
|
||||||
@@ -180,7 +180,9 @@ register_bundle(PoloniexBundle, ['USDT_BTC',])
|
|||||||
For a production environment make sure to use (to bundle all pairs):
|
For a production environment make sure to use (to bundle all pairs):
|
||||||
register_bundle(PoloniexBundle)
|
register_bundle(PoloniexBundle)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
if 'ingest' in sys.argv and '-c' in sys.argv:
|
if 'ingest' in sys.argv and '-c' in sys.argv:
|
||||||
register_bundle(PoloniexBundle)
|
register_bundle(PoloniexBundle)
|
||||||
else:
|
else:
|
||||||
register_bundle(PoloniexBundle, create_writers=False)
|
register_bundle(PoloniexBundle, create_writers=False)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from six.moves.urllib.parse import urlencode
|
from six.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
from catalyst.data.bundles.core import register_bundle
|
from catalyst.data.bundles.core import register_bundle
|
||||||
@@ -25,16 +26,25 @@ from catalyst.utils.memoize import lazyval
|
|||||||
"""
|
"""
|
||||||
Module for building a complete daily dataset from Quandl's WIKI dataset.
|
Module for building a complete daily dataset from Quandl's WIKI dataset.
|
||||||
"""
|
"""
|
||||||
|
from itertools import count
|
||||||
|
import tarfile
|
||||||
|
from time import time, sleep
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
import pandas as pd
|
||||||
|
from six.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
|
from catalyst.utils.calendars import register_calendar_alias
|
||||||
|
from catalyst.utils.cli import maybe_show_progress
|
||||||
|
|
||||||
|
from . import core as bundles
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.utils.calendars import register_calendar_alias
|
|
||||||
|
|
||||||
|
|
||||||
log = Logger(__name__, level=LOG_LEVEL)
|
log = Logger(__name__, level=LOG_LEVEL)
|
||||||
seconds_per_call = (pd.Timedelta('10 minutes') / 2000).total_seconds()
|
seconds_per_call = (pd.Timedelta('10 minutes') / 2000).total_seconds()
|
||||||
|
|
||||||
|
|
||||||
class QuandlBundle(BaseEquityPricingBundle):
|
class QuandlBundle(BaseEquityPricingBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def name(self):
|
def name(self):
|
||||||
@@ -99,8 +109,8 @@ class QuandlBundle(BaseEquityPricingBundle):
|
|||||||
# Filter out invalid symbols
|
# Filter out invalid symbols
|
||||||
raw = raw[~raw.symbol.isin(self._excluded_symbols)]
|
raw = raw[~raw.symbol.isin(self._excluded_symbols)]
|
||||||
|
|
||||||
# cut out all the other stuff in the name column. We need to
|
# cut out all the other stuff in the name column
|
||||||
# escape the paren because it is actually splitting on a regex
|
# we need to escape the paren because it is actually splitting on a regex
|
||||||
raw.asset_name = raw.asset_name.str.split(r' \(', 1).str.get(0)
|
raw.asset_name = raw.asset_name.str.split(r' \(', 1).str.get(0)
|
||||||
|
|
||||||
return raw
|
return raw
|
||||||
@@ -165,6 +175,7 @@ class QuandlBundle(BaseEquityPricingBundle):
|
|||||||
df['sid'] = asset_id
|
df['sid'] = asset_id
|
||||||
self.splits.append(df)
|
self.splits.append(df)
|
||||||
|
|
||||||
|
|
||||||
def _update_dividends(self, asset_id, raw_data):
|
def _update_dividends(self, asset_id, raw_data):
|
||||||
divs = raw_data.ex_dividend
|
divs = raw_data.ex_dividend
|
||||||
df = pd.DataFrame({'amount': divs[divs != 0]})
|
df = pd.DataFrame({'amount': divs[divs != 0]})
|
||||||
@@ -175,6 +186,7 @@ class QuandlBundle(BaseEquityPricingBundle):
|
|||||||
df['record_date'] = df['declared_date'] = df['pay_date'] = pd.NaT
|
df['record_date'] = df['declared_date'] = df['pay_date'] = pd.NaT
|
||||||
self.dividends.append(df)
|
self.dividends.append(df)
|
||||||
|
|
||||||
|
|
||||||
def _format_metadata_url(self, api_key, page_number):
|
def _format_metadata_url(self, api_key, page_number):
|
||||||
"""Build the query RL for the quandl WIKI metadata.
|
"""Build the query RL for the quandl WIKI metadata.
|
||||||
"""
|
"""
|
||||||
@@ -188,10 +200,10 @@ class QuandlBundle(BaseEquityPricingBundle):
|
|||||||
query_params = [('api_key', api_key)] + query_params
|
query_params = [('api_key', api_key)] + query_params
|
||||||
|
|
||||||
return (
|
return (
|
||||||
'https://www.quandl.com/api/v3/datasets.csv?'
|
'https://www.quandl.com/api/v3/datasets.csv?' + urlencode(query_params)
|
||||||
+ urlencode(query_params)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _format_wiki_url(self,
|
def _format_wiki_url(self,
|
||||||
api_key,
|
api_key,
|
||||||
symbol,
|
symbol,
|
||||||
@@ -217,6 +229,5 @@ class QuandlBundle(BaseEquityPricingBundle):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
register_calendar_alias('QUANDL', 'NYSE')
|
register_calendar_alias('QUANDL', 'NYSE')
|
||||||
register_bundle(QuandlBundle)
|
register_bundle(QuandlBundle)
|
||||||
|
|||||||
@@ -656,11 +656,11 @@ class DataPortal(object):
|
|||||||
return spot_value
|
return spot_value
|
||||||
|
|
||||||
def _get_minutely_spot_value(self,
|
def _get_minutely_spot_value(self,
|
||||||
asset,
|
asset,
|
||||||
column,
|
column,
|
||||||
dt,
|
dt,
|
||||||
data_frequency,
|
data_frequency,
|
||||||
ffill=False):
|
ffill=False):
|
||||||
|
|
||||||
reader = self._get_pricing_reader(data_frequency)
|
reader = self._get_pricing_reader(data_frequency)
|
||||||
|
|
||||||
|
|||||||
@@ -133,13 +133,11 @@ class AssetDispatchBarReader(with_metaclass(ABCMeta)):
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
class AssetDispatchMinuteBarReader(AssetDispatchBarReader):
|
class AssetDispatchMinuteBarReader(AssetDispatchBarReader):
|
||||||
|
|
||||||
def _dt_window_size(self, start_dt, end_dt):
|
def _dt_window_size(self, start_dt, end_dt):
|
||||||
return len(self.trading_calendar.minutes_in_range(start_dt, end_dt))
|
return len(self.trading_calendar.minutes_in_range(start_dt, end_dt))
|
||||||
|
|
||||||
|
|
||||||
class AssetDispatchSessionBarReader(AssetDispatchBarReader):
|
class AssetDispatchSessionBarReader(AssetDispatchBarReader):
|
||||||
|
|
||||||
def _dt_window_size(self, start_dt, end_dt):
|
def _dt_window_size(self, start_dt, end_dt):
|
||||||
|
|||||||
+80
-20
@@ -12,6 +12,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 datetime
|
||||||
import os
|
import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
@@ -22,7 +23,6 @@ from pandas_datareader.data import DataReader
|
|||||||
from six import iteritems
|
from six import iteritems
|
||||||
from six.moves.urllib_error import HTTPError
|
from six.moves.urllib_error import HTTPError
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.utils.calendars import get_calendar
|
from catalyst.utils.calendars import get_calendar
|
||||||
from . import treasuries, treasuries_can
|
from . import treasuries, treasuries_can
|
||||||
from .benchmarks import get_benchmark_returns
|
from .benchmarks import get_benchmark_returns
|
||||||
@@ -32,6 +32,8 @@ from ..utils.paths import (
|
|||||||
data_root,
|
data_root,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
logger = logbook.Logger('Loader', level=LOG_LEVEL)
|
logger = logbook.Logger('Loader', level=LOG_LEVEL)
|
||||||
|
|
||||||
# Mapping from index symbol to appropriate bond data
|
# Mapping from index symbol to appropriate bond data
|
||||||
@@ -127,13 +129,11 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
|
|||||||
# before this date.
|
# before this date.
|
||||||
'''
|
'''
|
||||||
if(bundle_data):
|
if(bundle_data):
|
||||||
# If we are using the bundle to retrieve the cryptobenchmark, find
|
# If we are using the bundle to retrieve the cryptobenchmark, find the last
|
||||||
# the last date for which there is trading data in the bundle
|
# date for which there is trading data in the bundle
|
||||||
asset = bundle_data.asset_finder.lookup_symbol(
|
asset = bundle_data.asset_finder.lookup_symbol(symbol=bm_symbol,as_of_date=None)
|
||||||
symbol=bm_symbol,as_of_date=None)
|
|
||||||
ix = bundle_data.daily_bar_reader._last_rows[asset.sid]
|
ix = bundle_data.daily_bar_reader._last_rows[asset.sid]
|
||||||
last_date = pd.to_datetime(
|
last_date = pd.to_datetime(bundle_data.daily_bar_reader._spot_col('day')[ix],unit='s')
|
||||||
bundle_data.daily_bar_reader._spot_col('day')[ix],unit='s')
|
|
||||||
else:
|
else:
|
||||||
last_date = trading_days[trading_days.get_loc(now, method='ffill') - 2]
|
last_date = trading_days[trading_days.get_loc(now, method='ffill') - 2]
|
||||||
'''
|
'''
|
||||||
@@ -142,7 +142,7 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
|
|||||||
if exchange is None:
|
if exchange is None:
|
||||||
# This is exceptional, since placing the import at the module scope
|
# This is exceptional, since placing the import at the module scope
|
||||||
# breaks things and it's only needed here
|
# breaks things and it's only needed here
|
||||||
from catalyst.exchange.utils.factory import get_exchange
|
from catalyst.exchange.factory import get_exchange
|
||||||
exchange = get_exchange(
|
exchange = get_exchange(
|
||||||
exchange_name='poloniex', base_currency='usdt'
|
exchange_name='poloniex', base_currency='usdt'
|
||||||
)
|
)
|
||||||
@@ -164,8 +164,8 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
|
|||||||
br.loc[start_dt] = 0
|
br.loc[start_dt] = 0
|
||||||
br = br.sort_index()
|
br = br.sort_index()
|
||||||
|
|
||||||
# Override first_date for treasury data since we have it for many more
|
# Override first_date for treasury data since we have it for many more years
|
||||||
# years and is independent of crypto data
|
# and is independent of crypto data
|
||||||
first_date_treasury = pd.Timestamp('1990-01-02', tz='UTC')
|
first_date_treasury = pd.Timestamp('1990-01-02', tz='UTC')
|
||||||
tc = ensure_treasury_data(
|
tc = ensure_treasury_data(
|
||||||
bm_symbol,
|
bm_symbol,
|
||||||
@@ -301,14 +301,14 @@ def ensure_crypto_benchmark_data(symbol,
|
|||||||
|
|
||||||
if (bundle == 'poloniex'):
|
if (bundle == 'poloniex'):
|
||||||
'''
|
'''
|
||||||
If we're using the Poloniex bundle, we'll get the benchmark from the
|
If we're using the Poloniex bundle, we'll get the benchmark from the bundle
|
||||||
bundle instead of downloading it from Poloniex every time we need it.
|
instead of downloading it from Poloniex every time we need it.
|
||||||
Poloniex has a captcha for API queries originating from outside the US
|
Poloniex has a captcha for API queries originating from outside the US that
|
||||||
that prevents users abroad from getting Catalyst to work
|
prevents users abroad from getting Catalyst to work
|
||||||
'''
|
'''
|
||||||
logger.info(
|
logger.info(
|
||||||
('Retrieving benchmark data from bundle for {symbol!r}'
|
(
|
||||||
' from {first_date} to {last_date}'),
|
'Retrieving benchmark data from bundle for {symbol!r} from {first_date} to {last_date}'),
|
||||||
symbol=symbol, first_date=first_date, last_date=last_date)
|
symbol=symbol, first_date=first_date, last_date=last_date)
|
||||||
|
|
||||||
asset = bundle_data.asset_finder.lookup_symbol(symbol=symbol,
|
asset = bundle_data.asset_finder.lookup_symbol(symbol=symbol,
|
||||||
@@ -331,11 +331,10 @@ def ensure_crypto_benchmark_data(symbol,
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# This is how it used to be: downloading the benchmark everytime.
|
# This is how it used to be: downloading the benchmark everytime.
|
||||||
# Leaving this code here to be repurposed in the future for
|
# Leaving this code here to be repurposed in the future for other bundles.
|
||||||
# other bundles.
|
|
||||||
logger.info(
|
logger.info(
|
||||||
('Downloading benchmark data for {symbol!r}'
|
(
|
||||||
' from {first_date} to {last_date}'),
|
'Downloading benchmark data for {symbol!r} from {first_date} to {last_date}'),
|
||||||
symbol=symbol, first_date=first_date, last_date=last_date)
|
symbol=symbol, first_date=first_date, last_date=last_date)
|
||||||
|
|
||||||
raise DeprecationWarning('poloniex bundle deprecated')
|
raise DeprecationWarning('poloniex bundle deprecated')
|
||||||
@@ -432,6 +431,67 @@ def ensure_benchmark_data(symbol, first_date, last_date, now, trading_day,
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_benchmark_data(symbol, first_date, last_date, now, trading_day,
|
||||||
|
environ=None):
|
||||||
|
"""
|
||||||
|
Ensure we have benchmark data for `symbol` from `first_date` to `last_date`
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
symbol : str
|
||||||
|
The symbol for the benchmark to load.
|
||||||
|
first_date : pd.Timestamp
|
||||||
|
First required date for the cache.
|
||||||
|
last_date : pd.Timestamp
|
||||||
|
Last required date for the cache.
|
||||||
|
now : pd.Timestamp
|
||||||
|
The current time. This is used to prevent repeated attempts to
|
||||||
|
re-download data that isn't available due to scheduling quirks or other
|
||||||
|
failures.
|
||||||
|
trading_day : pd.CustomBusinessDay
|
||||||
|
A trading day delta. Used to find the day before first_date so we can
|
||||||
|
get the close of the day prior to first_date.
|
||||||
|
|
||||||
|
We attempt to download data unless we already have data stored at the data
|
||||||
|
cache for `symbol` whose first entry is before or on `first_date` and whose
|
||||||
|
last entry is on or after `last_date`.
|
||||||
|
|
||||||
|
If we perform a download and the cache criteria are not satisfied, we wait
|
||||||
|
at least one hour before attempting a redownload. This is determined by
|
||||||
|
comparing the current time to the result of os.path.getmtime on the cache
|
||||||
|
path.
|
||||||
|
"""
|
||||||
|
filename = get_benchmark_filename(symbol)
|
||||||
|
data = _load_cached_data(filename, first_date, last_date, now, 'benchmark',
|
||||||
|
environ)
|
||||||
|
if data is not None:
|
||||||
|
return data
|
||||||
|
|
||||||
|
# If no cached data was found or it was missing any dates then download the
|
||||||
|
# necessary data.
|
||||||
|
logger.info(
|
||||||
|
('Downloading benchmark data for {symbol!r} '
|
||||||
|
'from {first_date} to {last_date}'),
|
||||||
|
symbol=symbol,
|
||||||
|
first_date=first_date - trading_day,
|
||||||
|
last_date=last_date
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = get_benchmark_returns(
|
||||||
|
symbol,
|
||||||
|
first_date - trading_day,
|
||||||
|
last_date,
|
||||||
|
)
|
||||||
|
data.to_csv(get_data_filepath(filename, environ))
|
||||||
|
except (OSError, IOError, HTTPError):
|
||||||
|
logger.exception('Failed to cache the new benchmark returns')
|
||||||
|
raise
|
||||||
|
if not has_data_for_dates(data, first_date, last_date):
|
||||||
|
logger.warn("Still don't have expected data after redownload!")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def ensure_treasury_data(symbol, first_date, last_date, now, environ=None):
|
def ensure_treasury_data(symbol, first_date, last_date, now, environ=None):
|
||||||
"""
|
"""
|
||||||
Ensure we have treasury data from treasury module associated with
|
Ensure we have treasury data from treasury module associated with
|
||||||
|
|||||||
@@ -341,10 +341,12 @@ class BcolzMinuteBarMetadata(object):
|
|||||||
'end_session': str(self.end_session.date()),
|
'end_session': str(self.end_session.date()),
|
||||||
# Write these values for backwards compatibility
|
# Write these values for backwards compatibility
|
||||||
'first_trading_day': str(self.start_session.date()),
|
'first_trading_day': str(self.start_session.date()),
|
||||||
'market_opens': (market_opens.values.astype('datetime64[m]').
|
'market_opens': (
|
||||||
astype(np.int64).tolist()),
|
market_opens.values.astype('datetime64[m]').
|
||||||
'market_closes': (market_closes.values.astype('datetime64[m]').
|
astype(np.int64).tolist()),
|
||||||
astype(np.int64).tolist()),
|
'market_closes': (
|
||||||
|
market_closes.values.astype('datetime64[m]').
|
||||||
|
astype(np.int64).tolist()),
|
||||||
}
|
}
|
||||||
with open(self.metadata_path(rootdir), 'w+') as fp:
|
with open(self.metadata_path(rootdir), 'w+') as fp:
|
||||||
json.dump(metadata, fp)
|
json.dump(metadata, fp)
|
||||||
@@ -1254,8 +1256,8 @@ class BcolzMinuteBarReader(MinuteBarReader):
|
|||||||
values = carray[start_idx:end_idx + 1]
|
values = carray[start_idx:end_idx + 1]
|
||||||
if indices_to_exclude is not None:
|
if indices_to_exclude is not None:
|
||||||
for excl_start, excl_stop in indices_to_exclude[::-1]:
|
for excl_start, excl_stop in indices_to_exclude[::-1]:
|
||||||
excl_slice = np.s_[excl_start - start_idx:excl_stop
|
excl_slice = np.s_[
|
||||||
- start_idx + 1]
|
excl_start - start_idx:excl_stop - start_idx + 1]
|
||||||
values = np.delete(values, excl_slice)
|
values = np.delete(values, excl_slice)
|
||||||
|
|
||||||
where = values != 0
|
where = values != 0
|
||||||
@@ -1318,8 +1320,9 @@ class H5MinuteBarUpdateWriter(object):
|
|||||||
|
|
||||||
def __init__(self, path, complevel=None, complib=None):
|
def __init__(self, path, complevel=None, complib=None):
|
||||||
self._complevel = complevel if complevel \
|
self._complevel = complevel if complevel \
|
||||||
is not None else self._COMPLEVEL
|
is not None else self._COMPLEVEL
|
||||||
self._complib = complib if complib is not None else self._COMPLIB
|
self._complib = complib if complib \
|
||||||
|
is not None else self._COMPLIB
|
||||||
self._path = path
|
self._path = path
|
||||||
|
|
||||||
def write(self, frames):
|
def write(self, frames):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
from __future__ import division # Python2 req for division of ints yield float
|
from __future__ import division # Python2 req to have division of ints yield float
|
||||||
|
|
||||||
from errno import ENOENT
|
from errno import ENOENT
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -120,8 +120,7 @@ SQLITE_STOCK_DIVIDEND_PAYOUT_COLUMN_DTYPES = {
|
|||||||
UINT32_MAX = iinfo(uint32).max
|
UINT32_MAX = iinfo(uint32).max
|
||||||
UINT64_MAX = iinfo(uint64).max
|
UINT64_MAX = iinfo(uint64).max
|
||||||
|
|
||||||
# Provides 9 decimals resolution. Also affects _equities.pyx L220
|
PRICE_ADJUSTMENT_FACTOR = 1000000000 # Provides 9 decimals resolution. Also affects _equities.pyx L220
|
||||||
PRICE_ADJUSTMENT_FACTOR = 1000000000
|
|
||||||
|
|
||||||
|
|
||||||
def check_uint32_safe(value, colname):
|
def check_uint32_safe(value, colname):
|
||||||
@@ -131,7 +130,6 @@ def check_uint32_safe(value, colname):
|
|||||||
"for uint32" % (value, colname)
|
"for uint32" % (value, colname)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_uint64_safe(value, colname):
|
def check_uint64_safe(value, colname):
|
||||||
if value >= UINT64_MAX:
|
if value >= UINT64_MAX:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -324,8 +322,8 @@ class BcolzDailyBarWriter(object):
|
|||||||
# Maps column name -> output carray.
|
# Maps column name -> output carray.
|
||||||
columns = {
|
columns = {
|
||||||
k: carray(array([], dtype=uint64))
|
k: carray(array([], dtype=uint64))
|
||||||
if k in OHLCV
|
if k in OHLCV
|
||||||
else carray(array([], dtype=uint32))
|
else carray(array([], dtype=uint32))
|
||||||
for k in US_EQUITY_PRICING_BCOLZ_COLUMNS
|
for k in US_EQUITY_PRICING_BCOLZ_COLUMNS
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,13 +439,11 @@ class BcolzDailyBarWriter(object):
|
|||||||
return raw_data
|
return raw_data
|
||||||
|
|
||||||
winsorise_uint64(raw_data, invalid_data_behavior, 'volume', *OHLC)
|
winsorise_uint64(raw_data, invalid_data_behavior, 'volume', *OHLC)
|
||||||
processed = (raw_data[list(OHLC)]
|
processed = (raw_data[list(OHLC)] * PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
||||||
* PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
|
||||||
dates = raw_data.index.values.astype('datetime64[s]')
|
dates = raw_data.index.values.astype('datetime64[s]')
|
||||||
check_uint32_safe(dates.max().view(np.int64), 'day')
|
check_uint32_safe(dates.max().view(np.int64), 'day')
|
||||||
processed['day'] = dates.astype('uint32')
|
processed['day'] = dates.astype('uint32')
|
||||||
processed['volume'] = (raw_data.volume
|
processed['volume'] = (raw_data.volume * PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
||||||
* PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
|
||||||
return ctable.fromdataframe(processed)
|
return ctable.fromdataframe(processed)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from catalyst.api import (
|
|||||||
symbol,
|
symbol,
|
||||||
get_open_orders
|
get_open_orders
|
||||||
)
|
)
|
||||||
from catalyst.exchange.utils.stats_utils import get_pretty_stats
|
from catalyst.exchange.stats_utils import get_pretty_stats
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
algo_namespace = 'arbitrage_eth_btc'
|
algo_namespace = 'arbitrage_eth_btc'
|
||||||
@@ -263,20 +263,13 @@ def analyze(context, stats):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
run_algorithm(
|
||||||
# The execution mode: backtest or live
|
initialize=initialize,
|
||||||
MODE = 'live'
|
handle_data=handle_data,
|
||||||
if MODE == 'live':
|
analyze=analyze,
|
||||||
run_algorithm(
|
exchange_name='poloniex,bitfinex',
|
||||||
capital_base=0.1,
|
live=True,
|
||||||
initialize=initialize,
|
algo_namespace=algo_namespace,
|
||||||
handle_data=handle_data,
|
quote_currency='btc',
|
||||||
analyze=analyze,
|
live_graph=False
|
||||||
exchange_name='poloniex,bitfinex',
|
)
|
||||||
live=True,
|
|
||||||
algo_namespace=algo_namespace,
|
|
||||||
base_currency='btc',
|
|
||||||
live_graph=False,
|
|
||||||
simulate_orders=True,
|
|
||||||
stats_output=None,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import matplotlib.pyplot as plt
|
|||||||
|
|
||||||
from catalyst import run_algorithm
|
from catalyst import run_algorithm
|
||||||
from catalyst.api import (order_target_value, symbol, record,
|
from catalyst.api import (order_target_value, symbol, record,
|
||||||
cancel_order, get_open_orders, )
|
cancel_order, get_open_orders, )
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
@@ -61,6 +61,7 @@ def handle_data(context, data):
|
|||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
record(
|
record(
|
||||||
|
|||||||
@@ -1,49 +1,30 @@
|
|||||||
'''
|
'''
|
||||||
This is a very simple example referenced in the beginner's tutorial:
|
This is a very simple example referenced in the beginner's tutorial:
|
||||||
https://enigmampc.github.io/catalyst/beginner-tutorial.html
|
https://enigmampc.github.io/catalyst/beginner-tutorial.html
|
||||||
|
|
||||||
Run this example, by executing the following from your terminal:
|
Run this example, by executing the following from your terminal:
|
||||||
catalyst ingest-exchange -x bitfinex -f daily -i btc_usdt
|
catalyst ingest-exchange -x bitfinex -f daily -i btc_usdt
|
||||||
catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 \
|
catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle
|
||||||
--end 2017-9-30 -o buy_btc_simple_out.pickle
|
|
||||||
|
|
||||||
If you want to run this code using another exchange, make sure that
|
If you want to run this code using another exchange, make sure that
|
||||||
the asset is available on that exchange. For example, if you were to run
|
the asset is available on that exchange. For example, if you were to run
|
||||||
it for exchange Poloniex, you would need to edit the following line:
|
it for exchange Poloniex, you would need to edit the following line:
|
||||||
|
|
||||||
context.asset = symbol('btc_usdt') # note 'usdt' instead of 'usd'
|
context.asset = symbol('btc_usdt') # note 'usdt' instead of 'usd'
|
||||||
|
|
||||||
and specify exchange poloniex as follows:
|
and specify exchange poloniex as follows:
|
||||||
catalyst ingest-exchange -x poloniex -f daily -i btc_usdt
|
catalyst ingest-exchange -x poloniex -f daily -i btc_usdt
|
||||||
catalyst run -f buy_btc_simple.py -x poloniex --start 2016-1-1 \
|
catalyst run -f buy_btc_simple.py -x poloniex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle
|
||||||
--end 2017-9-30 -o buy_btc_simple_out.pickle
|
|
||||||
|
|
||||||
To see which assets are available on each exchange, visit:
|
To see which assets are available on each exchange, visit:
|
||||||
https://www.enigma.co/catalyst/status
|
https://www.enigma.co/catalyst/status
|
||||||
'''
|
'''
|
||||||
from catalyst import run_algorithm
|
|
||||||
from catalyst.api import order, record, symbol
|
from catalyst.api import order, record, symbol
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.asset = symbol('btc_usd')
|
context.asset = symbol('btc_usd')
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
order(context.asset, 1)
|
order(context.asset, 1)
|
||||||
record(btc=data.current(context.asset, 'price'))
|
record(btc = data.current(context.asset, 'price'))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=10000,
|
|
||||||
data_frequency='daily',
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
exchange_name='bitfinex',
|
|
||||||
algo_namespace='buy_and_hodl',
|
|
||||||
base_currency='usd',
|
|
||||||
start=pd.to_datetime('2015-03-01', utc=True),
|
|
||||||
end=pd.to_datetime('2017-10-31', utc=True),
|
|
||||||
)
|
|
||||||
@@ -1,5 +1,15 @@
|
|||||||
|
'''
|
||||||
|
This algorithm requires an additional library (ta-lib) beyond those required by catalyst.
|
||||||
|
Install it first by running:
|
||||||
|
$ pip install TA-Lib
|
||||||
|
|
||||||
|
If you get build errors like "fatal error: ta-lib/ta_libc.h: No such file or directory"
|
||||||
|
it typically means that it can't find the underlying TA-Lib library and needs to be installed.
|
||||||
|
See https://mrjbq7.github.io/ta-lib/install.html for instructions on how to install
|
||||||
|
the required dependencies.
|
||||||
|
'''
|
||||||
|
|
||||||
import talib
|
import talib
|
||||||
import pandas as pd
|
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.api import (
|
from catalyst.api import (
|
||||||
@@ -9,52 +19,58 @@ from catalyst.api import (
|
|||||||
record,
|
record,
|
||||||
get_open_orders,
|
get_open_orders,
|
||||||
)
|
)
|
||||||
from catalyst.exchange.utils.stats_utils import get_pretty_stats
|
from catalyst.exchange.stats_utils import get_pretty_stats
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
|
||||||
|
|
||||||
algo_namespace = 'buy_the_dip_live'
|
algo_namespace = 'buy_low_sell_high_xrp'
|
||||||
log = Logger('buy low sell high')
|
log = Logger(algo_namespace)
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
log.info('initializing algo')
|
log.info('initializing algo')
|
||||||
context.ASSET_NAME = 'btc_usdt'
|
context.ASSET_NAME = 'XRP_USDT'
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
context.TARGET_POSITIONS = 30
|
context.TARGET_POSITIONS = 5000
|
||||||
context.PROFIT_TARGET = 0.1
|
context.PROFIT_TARGET = 0.1
|
||||||
context.SLIPPAGE_ALLOWED = 0.02
|
context.SLIPPAGE_ALLOWED = 0.05
|
||||||
|
|
||||||
|
context.retry_check_open_orders = 10
|
||||||
|
context.retry_update_portfolio = 10
|
||||||
|
context.retry_order = 5
|
||||||
|
|
||||||
|
context.swallow_errors = True
|
||||||
|
|
||||||
context.errors = []
|
context.errors = []
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _handle_data(context, data):
|
def _handle_data(context, data):
|
||||||
price = data.current(context.asset, 'price')
|
|
||||||
log.info('got price {price}'.format(price=price))
|
|
||||||
|
|
||||||
prices = data.history(
|
prices = data.history(
|
||||||
context.asset,
|
context.asset,
|
||||||
fields='price',
|
fields='price',
|
||||||
bar_count=20,
|
bar_count=20,
|
||||||
frequency='1D'
|
frequency='15m'
|
||||||
)
|
)
|
||||||
|
|
||||||
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
||||||
log.info('got rsi: {}'.format(rsi))
|
log.info('got rsi: {}'.format(rsi))
|
||||||
|
|
||||||
# Buying more when RSI is low, this should lower our cost basis
|
# Buying more when RSI is low, this should lower our cost basis
|
||||||
if rsi <= 30:
|
if rsi <= 30:
|
||||||
buy_increment = 1
|
buy_increment = 50
|
||||||
elif rsi <= 40:
|
elif rsi <= 40:
|
||||||
buy_increment = 0.5
|
buy_increment = 20
|
||||||
elif rsi <= 70:
|
elif rsi <= 70:
|
||||||
buy_increment = 0.2
|
buy_increment = 5
|
||||||
else:
|
else:
|
||||||
buy_increment = 0.1
|
buy_increment = None
|
||||||
|
|
||||||
cash = context.portfolio.cash
|
cash = context.portfolio.cash
|
||||||
log.info('base currency available: {cash}'.format(cash=cash))
|
log.info('base currency available: {cash}'.format(cash=cash))
|
||||||
|
|
||||||
|
price = data.current(context.asset, 'price')
|
||||||
|
log.info('got price {price}'.format(price=price))
|
||||||
|
|
||||||
record(
|
record(
|
||||||
price=price,
|
price=price,
|
||||||
rsi=rsi,
|
rsi=rsi,
|
||||||
@@ -84,8 +100,8 @@ def _handle_data(context, data):
|
|||||||
|
|
||||||
if price < cost_basis:
|
if price < cost_basis:
|
||||||
is_buy = True
|
is_buy = True
|
||||||
elif (position.amount > 0
|
elif position.amount > 0 and \
|
||||||
and price > cost_basis * (1 + context.PROFIT_TARGET)):
|
price > cost_basis * (1 + context.PROFIT_TARGET):
|
||||||
profit = (price * position.amount) - (cost_basis * position.amount)
|
profit = (price * position.amount) - (cost_basis * position.amount)
|
||||||
log.info('closing position, taking profit: {}'.format(profit))
|
log.info('closing position, taking profit: {}'.format(profit))
|
||||||
order_target_percent(
|
order_target_percent(
|
||||||
@@ -122,11 +138,11 @@ def _handle_data(context, data):
|
|||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
log.info('handling bar {}'.format(data.current_dt))
|
log.info('handling bar {}'.format(data.current_dt))
|
||||||
# try:
|
try:
|
||||||
_handle_data(context, data)
|
_handle_data(context, data)
|
||||||
# except Exception as e:
|
except Exception as e:
|
||||||
# log.warn('aborting the bar on error {}'.format(e))
|
log.warn('aborting the bar on error {}'.format(e))
|
||||||
# context.errors.append(e)
|
context.errors.append(e)
|
||||||
|
|
||||||
log.info('completed bar {}, total execution errors {}'.format(
|
log.info('completed bar {}, total execution errors {}'.format(
|
||||||
data.current_dt,
|
data.current_dt,
|
||||||
@@ -140,32 +156,3 @@ def handle_data(context, data):
|
|||||||
def analyze(context, stats):
|
def analyze(context, stats):
|
||||||
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
|
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
live = False
|
|
||||||
if live:
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=0.001,
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='binance',
|
|
||||||
live=True,
|
|
||||||
algo_namespace=algo_namespace,
|
|
||||||
base_currency='btc',
|
|
||||||
simulate_orders=True,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=10000,
|
|
||||||
data_frequency='daily',
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='poloniex',
|
|
||||||
algo_namespace='buy_and_hodl',
|
|
||||||
base_currency='usdt',
|
|
||||||
start=pd.to_datetime('2015-03-01', utc=True),
|
|
||||||
end=pd.to_datetime('2017-10-31', utc=True),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from catalyst.api import (
|
||||||
|
order,
|
||||||
|
order_target_percent,
|
||||||
|
symbol,
|
||||||
|
record,
|
||||||
|
get_open_orders,
|
||||||
|
)
|
||||||
|
from catalyst.exchange.stats_utils import get_pretty_stats
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
|
algo_namespace = 'buy_the_dip_live'
|
||||||
|
log = Logger('buy low sell high')
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
log.info('initializing algo')
|
||||||
|
context.ASSET_NAME = 'btc_usdt'
|
||||||
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
|
context.TARGET_POSITIONS = 30
|
||||||
|
context.PROFIT_TARGET = 0.1
|
||||||
|
context.SLIPPAGE_ALLOWED = 0.02
|
||||||
|
|
||||||
|
context.retry_check_open_orders = 10
|
||||||
|
context.retry_update_portfolio = 10
|
||||||
|
context.retry_order = 5
|
||||||
|
|
||||||
|
context.errors = []
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_data(context, data):
|
||||||
|
price = data.current(context.asset, 'price')
|
||||||
|
log.info('got price {price}'.format(price=price))
|
||||||
|
|
||||||
|
prices = data.history(
|
||||||
|
context.asset,
|
||||||
|
fields='price',
|
||||||
|
bar_count=20,
|
||||||
|
frequency='1d'
|
||||||
|
)
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
||||||
|
log.info('got rsi: {}'.format(rsi))
|
||||||
|
|
||||||
|
# Buying more when RSI is low, this should lower our cost basis
|
||||||
|
if rsi <= 30:
|
||||||
|
buy_increment = 1
|
||||||
|
elif rsi <= 40:
|
||||||
|
buy_increment = 0.5
|
||||||
|
elif rsi <= 70:
|
||||||
|
buy_increment = 0.2
|
||||||
|
else:
|
||||||
|
buy_increment = 0.1
|
||||||
|
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
log.info('base currency available: {cash}'.format(cash=cash))
|
||||||
|
|
||||||
|
record(
|
||||||
|
price=price,
|
||||||
|
rsi=rsi,
|
||||||
|
)
|
||||||
|
|
||||||
|
orders = get_open_orders(context.asset)
|
||||||
|
if orders:
|
||||||
|
log.info('skipping bar until all open orders execute')
|
||||||
|
return
|
||||||
|
|
||||||
|
is_buy = False
|
||||||
|
cost_basis = None
|
||||||
|
if context.asset in context.portfolio.positions:
|
||||||
|
position = context.portfolio.positions[context.asset]
|
||||||
|
|
||||||
|
cost_basis = position.cost_basis
|
||||||
|
log.info(
|
||||||
|
'found {amount} positions with cost basis {cost_basis}'.format(
|
||||||
|
amount=position.amount,
|
||||||
|
cost_basis=cost_basis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if position.amount >= context.TARGET_POSITIONS:
|
||||||
|
log.info('reached positions target: {}'.format(position.amount))
|
||||||
|
return
|
||||||
|
|
||||||
|
if price < cost_basis:
|
||||||
|
is_buy = True
|
||||||
|
elif position.amount > 0 and \
|
||||||
|
price > cost_basis * (1 + context.PROFIT_TARGET):
|
||||||
|
profit = (price * position.amount) - (cost_basis * position.amount)
|
||||||
|
log.info('closing position, taking profit: {}'.format(profit))
|
||||||
|
order_target_percent(
|
||||||
|
asset=context.asset,
|
||||||
|
target=0,
|
||||||
|
limit_price=price * (1 - context.SLIPPAGE_ALLOWED),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.info('no buy or sell opportunity found')
|
||||||
|
else:
|
||||||
|
is_buy = True
|
||||||
|
|
||||||
|
if is_buy:
|
||||||
|
if buy_increment is None:
|
||||||
|
log.info('the rsi is too high to consider buying {}'.format(rsi))
|
||||||
|
return
|
||||||
|
|
||||||
|
if price * buy_increment > cash:
|
||||||
|
log.info('not enough base currency to consider buying')
|
||||||
|
return
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'buying position cheaper than cost basis {} < {}'.format(
|
||||||
|
price,
|
||||||
|
cost_basis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
order(
|
||||||
|
asset=context.asset,
|
||||||
|
amount=buy_increment,
|
||||||
|
limit_price=price * (1 + context.SLIPPAGE_ALLOWED)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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, stats):
|
||||||
|
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=100000,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
start=pd.to_datetime('2017-5-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-10-16', utc=True),
|
||||||
|
base_currency='usdt',
|
||||||
|
data_frequency='daily'
|
||||||
|
)
|
||||||
|
# run_algorithm(
|
||||||
|
# initialize=initialize,
|
||||||
|
# handle_data=handle_data,
|
||||||
|
# analyze=analyze,
|
||||||
|
# exchange_name='poloniex',
|
||||||
|
# live=True,
|
||||||
|
# algo_namespace=algo_namespace,
|
||||||
|
# base_currency='btc'
|
||||||
|
# )
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
from catalyst import run_algorithm
|
||||||
from catalyst.api import (record, symbol, order_target_percent,
|
from catalyst.api import (order, record, symbol, order_target_percent,
|
||||||
get_open_orders)
|
get_open_orders)
|
||||||
from catalyst.exchange.utils.stats_utils import extract_transactions
|
from catalyst.exchange.stats_utils import extract_transactions
|
||||||
|
|
||||||
NAMESPACE = 'dual_moving_average'
|
NAMESPACE = 'dual_moving_average'
|
||||||
log = Logger(NAMESPACE)
|
log = Logger(NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.i = 0
|
context.i = 0
|
||||||
context.asset = symbol('ltc_usd')
|
context.asset = symbol('ltc_usd')
|
||||||
@@ -26,22 +25,16 @@ def handle_data(context, data):
|
|||||||
# Skip as many bars as long_window to properly compute the average
|
# Skip as many bars as long_window to properly compute the average
|
||||||
context.i += 1
|
context.i += 1
|
||||||
if context.i < long_window:
|
if context.i < long_window:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Compute moving averages calling data.history() for each
|
# Compute moving averages calling data.history() for each
|
||||||
# moving average with the appropriate parameters. We choose to use
|
# moving average with the appropriate parameters. We choose to use
|
||||||
# minute bars for this simulation -> freq="1m"
|
# minute bars for this simulation -> freq="1m"
|
||||||
# Returns a pandas dataframe.
|
# Returns a pandas dataframe.
|
||||||
short_mavg = data.history(context.asset,
|
short_mavg = data.history(context.asset, 'price',
|
||||||
'price',
|
bar_count=short_window, frequency="1m").mean()
|
||||||
bar_count=short_window,
|
long_mavg = data.history(context.asset, 'price',
|
||||||
frequency="1m",
|
bar_count=long_window, frequency="1m").mean()
|
||||||
).mean()
|
|
||||||
long_mavg = data.history(context.asset,
|
|
||||||
'price',
|
|
||||||
bar_count=long_window,
|
|
||||||
frequency="1m",
|
|
||||||
).mean()
|
|
||||||
|
|
||||||
# Let's keep the price of our asset in a more handy variable
|
# Let's keep the price of our asset in a more handy variable
|
||||||
price = data.current(context.asset, 'price')
|
price = data.current(context.asset, 'price')
|
||||||
@@ -74,11 +67,11 @@ def handle_data(context, data):
|
|||||||
|
|
||||||
# Trading logic
|
# Trading logic
|
||||||
if short_mavg > long_mavg and pos_amount == 0:
|
if short_mavg > long_mavg and pos_amount == 0:
|
||||||
# we buy 100% of our portfolio for this asset
|
# we buy 100% of our portfolio for this asset
|
||||||
order_target_percent(context.asset, 1)
|
order_target_percent(context.asset, 1)
|
||||||
elif short_mavg < long_mavg and pos_amount > 0:
|
elif short_mavg < long_mavg and pos_amount > 0:
|
||||||
# we sell all our positions for this asset
|
# we sell all our positions for this asset
|
||||||
order_target_percent(context.asset, 0)
|
order_target_percent(context.asset, 0)
|
||||||
|
|
||||||
|
|
||||||
def analyze(context, perf):
|
def analyze(context, perf):
|
||||||
@@ -96,13 +89,11 @@ def analyze(context, perf):
|
|||||||
|
|
||||||
# Second chart: Plot asset price, moving averages and buys/sells
|
# Second chart: Plot asset price, moving averages and buys/sells
|
||||||
ax2 = plt.subplot(412, sharex=ax1)
|
ax2 = plt.subplot(412, sharex=ax1)
|
||||||
perf.loc[:, ['price', 'short_mavg', 'long_mavg']].plot(
|
perf.loc[:, ['price','short_mavg','long_mavg']].plot(ax=ax2, label='Price')
|
||||||
ax=ax2,
|
|
||||||
label='Price')
|
|
||||||
ax2.legend_.remove()
|
ax2.legend_.remove()
|
||||||
ax2.set_ylabel('{asset}\n({base})'.format(
|
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||||
asset=context.asset.symbol,
|
asset = context.asset.symbol,
|
||||||
base=base_currency
|
base = base_currency
|
||||||
))
|
))
|
||||||
start, end = ax2.get_ylim()
|
start, end = ax2.get_ylim()
|
||||||
ax2.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
ax2.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Copyright 2017 Enigma MPC, Inc.
|
||||||
|
# Copyright 2014 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.
|
||||||
|
|
||||||
|
from catalyst.api import (
|
||||||
|
order_target_percent,
|
||||||
|
record,
|
||||||
|
symbol,
|
||||||
|
get_open_orders,
|
||||||
|
set_max_leverage,
|
||||||
|
schedule_function,
|
||||||
|
date_rules,
|
||||||
|
attach_pipeline,
|
||||||
|
pipeline_output,
|
||||||
|
)
|
||||||
|
|
||||||
|
from catalyst.pipeline import Pipeline
|
||||||
|
from catalyst.pipeline.data import CryptoPricing
|
||||||
|
from catalyst.pipeline.factors.crypto import VWAP
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.ASSET_NAME = 'USDT_BTC'
|
||||||
|
context.TARGET_INVESTMENT_RATIO = 0.8
|
||||||
|
context.SHORT_WINDOW = 30
|
||||||
|
context.LONG_WINDOW = 100
|
||||||
|
|
||||||
|
# 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.i = 0
|
||||||
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
|
set_max_leverage(1.0)
|
||||||
|
|
||||||
|
attach_pipeline(make_pipeline(context), 'vwap_pipeline')
|
||||||
|
|
||||||
|
schedule_function(
|
||||||
|
rebalance,
|
||||||
|
time_rules=times_rules.every_minute(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def before_trading_start(context, data):
|
||||||
|
context.pipeline_data = pipeline_output('vwap_pipeline')
|
||||||
|
|
||||||
|
def make_pipeline(context):
|
||||||
|
return Pipeline(
|
||||||
|
columns={
|
||||||
|
'price': CryptoPricing.open.latest,
|
||||||
|
'volume': CryptoPricing.volume.latest,
|
||||||
|
'short_mavg': VWAP(window_length=context.SHORT_WINDOW),
|
||||||
|
'long_mavg': VWAP(window_length=context.LONG_WINDOW),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def rebalance(context, data):
|
||||||
|
context.i += 1
|
||||||
|
|
||||||
|
# skip first LONG_WINDOW bars to fill windows
|
||||||
|
if context.i < context.LONG_WINDOW:
|
||||||
|
return
|
||||||
|
|
||||||
|
# get pipeline data for asset of interest
|
||||||
|
pipeline_data = context.pipeline_data
|
||||||
|
pipeline_data = pipeline_data[pipeline_data.index == context.asset].iloc[0]
|
||||||
|
|
||||||
|
# retrieve long and short moving averages from pipeline
|
||||||
|
short_mavg = pipeline_data.short_mavg
|
||||||
|
long_mavg = pipeline_data.long_mavg
|
||||||
|
price = pipeline_data.price
|
||||||
|
volume = pipeline_data.volume
|
||||||
|
|
||||||
|
# check that order has not already been placed
|
||||||
|
open_orders = get_open_orders()
|
||||||
|
if context.asset not in open_orders:
|
||||||
|
# check that the asset of interest can currently be traded
|
||||||
|
if data.can_trade(context.asset):
|
||||||
|
# adjust portfolio based on comparison of long and short vwap
|
||||||
|
if short_mavg > long_mavg:
|
||||||
|
order_target_percent(
|
||||||
|
context.asset,
|
||||||
|
context.TARGET_INVESTMENT_RATIO,
|
||||||
|
)
|
||||||
|
elif short_mavg < long_mavg:
|
||||||
|
order_target_percent(
|
||||||
|
context.asset,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
record(
|
||||||
|
price=price,
|
||||||
|
cash=context.portfolio.cash,
|
||||||
|
leverage=context.account.leverage,
|
||||||
|
short_mavg=short_mavg,
|
||||||
|
long_mavg=long_mavg,
|
||||||
|
volume=volume,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
(context.TICK_SIZE*results[['price', 'short_mavg', 'long_mavg']]).plot(ax=ax2)
|
||||||
|
|
||||||
|
trans = results.ix[[t != [] for t in results.transactions]]
|
||||||
|
amounts = [t[0]['amount'] for t in trans.transactions]
|
||||||
|
|
||||||
|
buys = trans.ix[
|
||||||
|
[t[0]['amount'] > 0 for t in trans.transactions]
|
||||||
|
]
|
||||||
|
sells = trans.ix[
|
||||||
|
[t[0]['amount'] < 0 for t in trans.transactions]
|
||||||
|
]
|
||||||
|
|
||||||
|
ax2.plot(
|
||||||
|
buys.index,
|
||||||
|
context.TICK_SIZE * results.price[buys.index],
|
||||||
|
'^',
|
||||||
|
markersize=10,
|
||||||
|
color='g',
|
||||||
|
)
|
||||||
|
ax2.plot(
|
||||||
|
sells.index,
|
||||||
|
context.TICK_SIZE * results.price[sells.index],
|
||||||
|
'v',
|
||||||
|
markersize=10,
|
||||||
|
color='r',
|
||||||
|
)
|
||||||
|
|
||||||
|
ax3 = plt.subplot(613, sharex=ax1)
|
||||||
|
results[['leverage', 'alpha', 'beta']].plot(ax=ax3)
|
||||||
|
ax3.set_ylabel('Leverage (USD)')
|
||||||
|
|
||||||
|
ax4 = plt.subplot(614, sharex=ax1)
|
||||||
|
results[['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 (mBTC/day)')
|
||||||
|
|
||||||
|
plt.legend(loc=3)
|
||||||
|
|
||||||
|
# Show the plot.
|
||||||
|
plt.gcf().set_size_inches(18, 8)
|
||||||
|
plt.show()
|
||||||
@@ -12,7 +12,7 @@ from logbook import Logger
|
|||||||
|
|
||||||
from catalyst import run_algorithm
|
from catalyst import run_algorithm
|
||||||
from catalyst.api import symbol, record, order_target_percent, get_open_orders
|
from catalyst.api import symbol, record, order_target_percent, get_open_orders
|
||||||
from catalyst.exchange.utils.stats_utils import extract_transactions
|
from catalyst.exchange.stats_utils import extract_transactions
|
||||||
# We give a name to the algorithm which Catalyst will use to persist its state.
|
# 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`
|
# In this example, Catalyst will create the `.catalyst/data/live_algos`
|
||||||
# directory. If we stop and start the algorithm, Catalyst will resume its
|
# directory. If we stop and start the algorithm, Catalyst will resume its
|
||||||
@@ -33,19 +33,16 @@ def initialize(context):
|
|||||||
# parameters or values you're going to use.
|
# parameters or values you're going to use.
|
||||||
|
|
||||||
# In our example, we're looking at Neo in Ether.
|
# In our example, we're looking at Neo in Ether.
|
||||||
context.market = symbol('eth_btc')
|
context.neo_eth = symbol('neo_eth')
|
||||||
context.base_price = None
|
context.base_price = None
|
||||||
context.current_day = None
|
context.current_day = None
|
||||||
|
|
||||||
context.RSI_OVERSOLD = 50
|
context.RSI_OVERSOLD = 55
|
||||||
context.RSI_OVERBOUGHT = 65
|
context.RSI_OVERBOUGHT = 82
|
||||||
context.CANDLE_SIZE = '5T'
|
context.CANDLE_SIZE = '5T'
|
||||||
|
|
||||||
context.start_time = time.time()
|
context.start_time = time.time()
|
||||||
|
|
||||||
# context.set_commission(maker=0.1, taker=0.2)
|
|
||||||
context.set_slippage(spread=0.0001)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
# This handle_data function is where the real work is done. Our data is
|
# This handle_data function is where the real work is done. Our data is
|
||||||
@@ -62,14 +59,14 @@ def handle_data(context, data):
|
|||||||
context.current_day = today
|
context.current_day = today
|
||||||
|
|
||||||
# We're computing the volume-weighted-average-price of the security
|
# We're computing the volume-weighted-average-price of the security
|
||||||
# defined above, in the context.market variable. For this example, we're
|
# defined above, in the context.neo_eth variable. For this example, we're
|
||||||
# using three bars on the 15 min bars.
|
# using three bars on the 15 min bars.
|
||||||
|
|
||||||
# The frequency attribute determine the bar size. We use this convention
|
# The frequency attribute determine the bar size. We use this convention
|
||||||
# for the frequency alias:
|
# for the frequency alias:
|
||||||
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
|
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
|
||||||
prices = data.history(
|
prices = data.history(
|
||||||
context.market,
|
context.neo_eth,
|
||||||
fields='close',
|
fields='close',
|
||||||
bar_count=50,
|
bar_count=50,
|
||||||
frequency=context.CANDLE_SIZE
|
frequency=context.CANDLE_SIZE
|
||||||
@@ -84,7 +81,7 @@ def handle_data(context, data):
|
|||||||
# We need a variable for the current price of the security to compare to
|
# We need a variable for the current price of the security to compare to
|
||||||
# the average. Since we are requesting two fields, data.current()
|
# the average. Since we are requesting two fields, data.current()
|
||||||
# returns a DataFrame with
|
# returns a DataFrame with
|
||||||
current = data.current(context.market, fields=['close', 'volume'])
|
current = data.current(context.neo_eth, fields=['close', 'volume'])
|
||||||
price = current['close']
|
price = current['close']
|
||||||
|
|
||||||
# If base_price is not set, we use the current value. This is the
|
# If base_price is not set, we use the current value. This is the
|
||||||
@@ -98,36 +95,34 @@ def handle_data(context, data):
|
|||||||
# Now that we've collected all current data for this frame, we use
|
# 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
|
# the record() method to save it. This data will be available as
|
||||||
# a parameter of the analyze() function for further analysis.
|
# a parameter of the analyze() function for further analysis.
|
||||||
|
|
||||||
record(
|
record(
|
||||||
volume=current['volume'],
|
|
||||||
price=price,
|
price=price,
|
||||||
|
volume=current['volume'],
|
||||||
price_change=price_change,
|
price_change=price_change,
|
||||||
rsi=rsi[-1],
|
rsi=rsi[-1],
|
||||||
cash=cash
|
cash=cash
|
||||||
)
|
)
|
||||||
|
|
||||||
# We are trying to avoid over-trading by limiting our trades to
|
# We are trying to avoid over-trading by limiting our trades to
|
||||||
# one per day.
|
# one per day.
|
||||||
if context.traded_today:
|
if context.traded_today:
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO: retest with open orders
|
|
||||||
# Since we are using limit orders, some orders may not execute immediately
|
# Since we are using limit orders, some orders may not execute immediately
|
||||||
# we wait until all orders are executed before considering more trades.
|
# we wait until all orders are executed before considering more trades.
|
||||||
orders = get_open_orders(context.market)
|
orders = get_open_orders(context.neo_eth)
|
||||||
if len(orders) > 0:
|
if len(orders) > 0:
|
||||||
log.info('exiting because orders are open: {}'.format(orders))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Exit if we cannot trade
|
# Exit if we cannot trade
|
||||||
if not data.can_trade(context.market):
|
if not data.can_trade(context.neo_eth):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Another powerful built-in feature of the Catalyst backtester is the
|
# Another powerful built-in feature of the Catalyst backtester is the
|
||||||
# portfolio object. The portfolio object tracks your positions, cash,
|
# portfolio object. The portfolio object tracks your positions, cash,
|
||||||
# cost basis of specific holdings, and more. In this line, we calculate
|
# cost basis of specific holdings, and more. In this line, we calculate
|
||||||
# how long or short our position is at this minute.
|
# how long or short our position is at this minute.
|
||||||
pos_amount = context.portfolio.positions[context.market].amount
|
pos_amount = context.portfolio.positions[context.neo_eth].amount
|
||||||
|
|
||||||
if rsi[-1] <= context.RSI_OVERSOLD and pos_amount == 0:
|
if rsi[-1] <= context.RSI_OVERSOLD and pos_amount == 0:
|
||||||
log.info(
|
log.info(
|
||||||
@@ -138,7 +133,7 @@ def handle_data(context, data):
|
|||||||
# Set a style for limit orders,
|
# Set a style for limit orders,
|
||||||
limit_price = price * 1.005
|
limit_price = price * 1.005
|
||||||
order_target_percent(
|
order_target_percent(
|
||||||
context.market, 1, limit_price=limit_price
|
context.neo_eth, 1, limit_price=limit_price
|
||||||
)
|
)
|
||||||
context.traded_today = True
|
context.traded_today = True
|
||||||
|
|
||||||
@@ -150,7 +145,7 @@ def handle_data(context, data):
|
|||||||
)
|
)
|
||||||
limit_price = price * 0.995
|
limit_price = price * 0.995
|
||||||
order_target_percent(
|
order_target_percent(
|
||||||
context.market, 0, limit_price=limit_price
|
context.neo_eth, 0, limit_price=limit_price
|
||||||
)
|
)
|
||||||
context.traded_today = True
|
context.traded_today = True
|
||||||
|
|
||||||
@@ -173,7 +168,7 @@ def analyze(context=None, perf=None):
|
|||||||
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
|
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
|
||||||
|
|
||||||
ax2.set_ylabel('{asset}\n({base})'.format(
|
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||||
asset=context.market.symbol, base=base_currency
|
asset=context.neo_eth.symbol, base=base_currency
|
||||||
))
|
))
|
||||||
|
|
||||||
transaction_df = extract_transactions(perf)
|
transaction_df = extract_transactions(perf)
|
||||||
@@ -234,7 +229,7 @@ def analyze(context=None, perf=None):
|
|||||||
)
|
)
|
||||||
plt.legend(loc=3)
|
plt.legend(loc=3)
|
||||||
start, end = ax6.get_ylim()
|
start, end = ax6.get_ylim()
|
||||||
ax6.yaxis.set_ticks(np.arange(0, end, end / 5))
|
ax6.yaxis.set_ticks(np.arange(0, end, end/5))
|
||||||
|
|
||||||
# Show the plot.
|
# Show the plot.
|
||||||
plt.gcf().set_size_inches(18, 8)
|
plt.gcf().set_size_inches(18, 8)
|
||||||
@@ -244,24 +239,9 @@ def analyze(context=None, perf=None):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# The execution mode: backtest or live
|
# The execution mode: backtest or live
|
||||||
live = False
|
MODE = 'live'
|
||||||
|
|
||||||
if live:
|
if MODE == 'backtest':
|
||||||
run_algorithm(
|
|
||||||
capital_base=0.03,
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='poloniex',
|
|
||||||
live=True,
|
|
||||||
algo_namespace=NAMESPACE,
|
|
||||||
base_currency='btc',
|
|
||||||
live_graph=False,
|
|
||||||
simulate_orders=False,
|
|
||||||
stats_output=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
folder = os.path.join(
|
folder = os.path.join(
|
||||||
tempfile.gettempdir(), 'catalyst', NAMESPACE
|
tempfile.gettempdir(), 'catalyst', NAMESPACE
|
||||||
)
|
)
|
||||||
@@ -269,9 +249,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
timestr = time.strftime('%Y%m%d-%H%M%S')
|
timestr = time.strftime('%Y%m%d-%H%M%S')
|
||||||
out = os.path.join(folder, '{}.p'.format(timestr))
|
out = os.path.join(folder, '{}.p'.format(timestr))
|
||||||
# catalyst run -f catalyst/examples/mean_reversion_simple.py \
|
# catalyst run -f catalyst/examples/mean_reversion_simple.py -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000
|
||||||
# -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion \
|
|
||||||
# --data-frequency minute --capital-base 10000
|
|
||||||
run_algorithm(
|
run_algorithm(
|
||||||
capital_base=0.1,
|
capital_base=0.1,
|
||||||
data_frequency='minute',
|
data_frequency='minute',
|
||||||
@@ -286,3 +264,16 @@ if __name__ == '__main__':
|
|||||||
output=out
|
output=out
|
||||||
)
|
)
|
||||||
log.info('saved perf stats: {}'.format(out))
|
log.info('saved perf stats: {}'.format(out))
|
||||||
|
|
||||||
|
elif MODE == 'live':
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.1,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bittrex',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='eth',
|
||||||
|
live_graph=False
|
||||||
|
)
|
||||||
|
|||||||
@@ -15,135 +15,119 @@ import os
|
|||||||
import pytz
|
import pytz
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from scipy.optimize import minimize
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from catalyst.api import record, symbols, order_target_percent
|
from catalyst.api import record, symbol, symbols, order_target_percent
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
np.set_printoptions(threshold='nan', suppress=True)
|
np.set_printoptions(threshold='nan', suppress=True)
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
# Portfolio assets list
|
# Portfolio assets list
|
||||||
context.assets = symbols('btc_usdt', 'eth_usdt', 'ltc_usdt', 'dash_usdt',
|
context.assets = symbols('btc_usdt', 'eth_usdt', 'ltc_usdt', 'dash_usdt',
|
||||||
'xmr_usdt')
|
'xmr_usdt')
|
||||||
context.nassets = len(context.assets)
|
context.nassets = len(context.assets)
|
||||||
# Set the time window that will be used to compute expected return
|
# Set the time window that will be used to compute expected return
|
||||||
# and asset correlations
|
# and asset correlations
|
||||||
context.window = 180
|
context.window = 180
|
||||||
# Set the number of days between each portfolio rebalancing
|
# Set the number of days between each portfolio rebalancing
|
||||||
context.rebalance_period = 30
|
context.rebalance_period = 30
|
||||||
context.i = 0
|
context.i = 0
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
# Only rebalance at the beggining of the algorithm execution and
|
# Only rebalance at the beggining of the algorithm execution and
|
||||||
# every multiple of the rebalance period
|
# every multiple of the rebalance period
|
||||||
if context.i == 0 or context.i % context.rebalance_period == 0:
|
if context.i == 0 or context.i%context.rebalance_period == 0:
|
||||||
n = context.window
|
n = context.window
|
||||||
prices = data.history(context.assets, fields='price',
|
prices = data.history(context.assets, fields='price',
|
||||||
bar_count=n + 1, frequency='1d')
|
bar_count=n+1, frequency='1d')
|
||||||
pr = np.asmatrix(prices)
|
pr = np.asmatrix(prices)
|
||||||
t_prices = prices.iloc[1:n + 1]
|
t_prices = prices.iloc[1:n+1]
|
||||||
t_val = t_prices.values
|
t_val = t_prices.values
|
||||||
tminus_prices = prices.iloc[0:n]
|
tminus_prices = prices.iloc[0:n]
|
||||||
tminus_val = tminus_prices.values
|
tminus_val = tminus_prices.values
|
||||||
# Compute daily returns (r)
|
# Compute daily returns (r)
|
||||||
r = np.asmatrix(t_val / tminus_val - 1)
|
r = np.asmatrix(t_val/tminus_val-1)
|
||||||
# Compute the expected returns of each asset with the average
|
# Compute the expected returns of each asset with the average
|
||||||
# daily return for the selected time window
|
# daily return for the selected time window
|
||||||
m = np.asmatrix(np.mean(r, axis=0))
|
m = np.asmatrix(np.mean(r, axis=0))
|
||||||
# ###
|
# ###
|
||||||
stds = np.std(r, axis=0)
|
stds = np.std(r, axis=0)
|
||||||
# Compute excess returns matrix (xr)
|
# Compute excess returns matrix (xr)
|
||||||
xr = r - m
|
xr = r - m
|
||||||
# Matrix algebra to get variance-covariance matrix
|
# Matrix algebra to get variance-covariance matrix
|
||||||
cov_m = np.dot(np.transpose(xr), xr) / n
|
cov_m = np.dot(np.transpose(xr),xr)/n
|
||||||
# Compute asset correlation matrix (informative only)
|
# Compute asset correlation matrix (informative only)
|
||||||
corr_m = cov_m / np.dot(np.transpose(stds), stds)
|
corr_m = cov_m/np.dot(np.transpose(stds),stds)
|
||||||
|
|
||||||
# Define portfolio optimization parameters
|
# Define portfolio optimization parameters
|
||||||
n_portfolios = 50000
|
n_portfolios = 50000
|
||||||
results_array = np.zeros((3 + context.nassets, n_portfolios))
|
results_array = np.zeros((3+context.nassets,n_portfolios))
|
||||||
for p in xrange(n_portfolios):
|
for p in xrange(n_portfolios):
|
||||||
weights = np.random.random(context.nassets)
|
weights = np.random.random(context.nassets)
|
||||||
weights /= np.sum(weights)
|
weights /= np.sum(weights)
|
||||||
w = np.asmatrix(weights)
|
w = np.asmatrix(weights)
|
||||||
p_r = np.sum(np.dot(w, np.transpose(m))) * 365
|
p_r = np.sum(np.dot(w,np.transpose(m)))*365
|
||||||
p_std = np.sqrt(np.dot(np.dot(w, cov_m),
|
p_std = np.sqrt(np.dot(np.dot(w,cov_m),np.transpose(w)))*np.sqrt(365)
|
||||||
np.transpose(w))) * np.sqrt(365)
|
|
||||||
|
|
||||||
# store results in results array
|
#store results in results array
|
||||||
results_array[0, p] = p_r
|
results_array[0,p] = p_r
|
||||||
results_array[1, p] = p_std
|
results_array[1,p] = p_std
|
||||||
# store Sharpe Ratio (return / volatility) - risk free rate element
|
#store Sharpe Ratio (return / volatility) - risk free rate element
|
||||||
# excluded for simplicity
|
#excluded for simplicity
|
||||||
results_array[2, p] = results_array[0, p] / results_array[1, p]
|
results_array[2,p] = results_array[0,p] / results_array[1,p]
|
||||||
i = 0
|
i = 0
|
||||||
for iw in weights:
|
for iw in weights:
|
||||||
results_array[3 + i, p] = weights[i]
|
results_array[3+i,p] = weights[i]
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
# convert results array to Pandas DataFrame
|
#convert results array to Pandas DataFrame
|
||||||
results_frame = pd.DataFrame(np.transpose(results_array),
|
results_frame = pd.DataFrame(np.transpose(results_array),
|
||||||
columns=['r', 'stdev', 'sharpe']
|
columns=['r','stdev','sharpe']+context.assets)
|
||||||
+ context.assets)
|
#locate position of portfolio with highest Sharpe Ratio
|
||||||
# locate position of portfolio with highest Sharpe Ratio
|
max_sharpe_port = results_frame.iloc[results_frame['sharpe'].idxmax()]
|
||||||
max_sharpe_port = results_frame.iloc[results_frame['sharpe'].idxmax()]
|
#locate positon of portfolio with minimum standard deviation
|
||||||
# locate positon of portfolio with minimum standard deviation
|
min_vol_port = results_frame.iloc[results_frame['stdev'].idxmin()]
|
||||||
# min_vol_port = results_frame.iloc[results_frame['stdev'].idxmin()]
|
|
||||||
|
|
||||||
# order optimal weights for each asset
|
#order optimal weights for each asset
|
||||||
for asset in context.assets:
|
for asset in context.assets:
|
||||||
if data.can_trade(asset):
|
if data.can_trade(asset):
|
||||||
order_target_percent(asset, max_sharpe_port[asset])
|
order_target_percent(asset, max_sharpe_port[asset])
|
||||||
|
|
||||||
# create scatter plot coloured by Sharpe Ratio
|
#create scatter plot coloured by Sharpe Ratio
|
||||||
plt.scatter(results_frame.stdev,
|
plt.scatter(results_frame.stdev,results_frame.r,c=results_frame.sharpe,cmap='RdYlGn')
|
||||||
results_frame.r,
|
plt.xlabel('Volatility')
|
||||||
c=results_frame.sharpe,
|
plt.ylabel('Returns')
|
||||||
cmap='RdYlGn')
|
plt.colorbar()
|
||||||
plt.xlabel('Volatility')
|
#plot red star to highlight position of portfolio with highest Sharpe Ratio
|
||||||
plt.ylabel('Returns')
|
plt.scatter(max_sharpe_port[1],max_sharpe_port[0],marker='o',color='b',s=200)
|
||||||
plt.colorbar()
|
#plot green star to highlight position of minimum variance portfolio
|
||||||
# plot red star to highlight position of portfolio
|
plt.show()
|
||||||
# with highest Sharpe Ratio
|
print(max_sharpe_port)
|
||||||
plt.scatter(max_sharpe_port[1],
|
record(pr=pr,r=r, m=m, stds=stds ,max_sharpe_port=max_sharpe_port, corr_m=corr_m)
|
||||||
max_sharpe_port[0],
|
context.i += 1
|
||||||
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):
|
def analyze(context=None, results=None):
|
||||||
# Form DataFrame with selected data
|
# Form DataFrame with selected data
|
||||||
data = results[['pr', 'r', 'm', 'stds', 'max_sharpe_port', 'corr_m',
|
data = results[['pr','r','m','stds','max_sharpe_port','corr_m','portfolio_value']]
|
||||||
'portfolio_value']]
|
|
||||||
|
|
||||||
# Save results in CSV file
|
# Save results in CSV file
|
||||||
filename = os.path.splitext(os.path.basename(__file__))[0]
|
filename = os.path.splitext(os.path.basename(__file__))[0]
|
||||||
data.to_csv(filename + '.csv')
|
data.to_csv(filename + '.csv')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
# Bitcoin data is available from 2015-3-2. Dates vary for other tokens.
|
||||||
# Bitcoin data is available from 2015-3-2. Dates vary for other tokens.
|
start = datetime(2017, 1, 1, 0, 0, 0, 0, pytz.utc)
|
||||||
start = datetime(2017, 1, 1, 0, 0, 0, 0, pytz.utc)
|
end = datetime(2017, 8, 16, 0, 0, 0, 0, pytz.utc)
|
||||||
end = datetime(2017, 8, 16, 0, 0, 0, 0, pytz.utc)
|
results = run_algorithm(initialize=initialize,
|
||||||
results = run_algorithm(initialize=initialize,
|
handle_data=handle_data,
|
||||||
handle_data=handle_data,
|
analyze=analyze,
|
||||||
analyze=analyze,
|
start=start,
|
||||||
start=start,
|
end=end,
|
||||||
end=end,
|
exchange_name='poloniex',
|
||||||
exchange_name='poloniex',
|
capital_base=100000, )
|
||||||
capital_base=100000, )
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from catalyst.api import (
|
|||||||
record,
|
record,
|
||||||
get_open_orders,
|
get_open_orders,
|
||||||
)
|
)
|
||||||
|
from catalyst.exchange.stats_utils import crossover, crossunder
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
algo_namespace = 'rsi'
|
algo_namespace = 'rsi'
|
||||||
@@ -54,7 +55,7 @@ def _handle_buy_sell_decision(context, data, signal, price):
|
|||||||
stop=None
|
stop=None
|
||||||
)
|
)
|
||||||
|
|
||||||
# action = None
|
action = None
|
||||||
if context.position is not None:
|
if context.position is not None:
|
||||||
cost_basis = context.position['cost_basis']
|
cost_basis = context.position['cost_basis']
|
||||||
amount = context.position['amount']
|
amount = context.position['amount']
|
||||||
@@ -79,7 +80,7 @@ def _handle_buy_sell_decision(context, data, signal, price):
|
|||||||
amount=-amount,
|
amount=-amount,
|
||||||
limit_price=price * (1 - context.SLIPPAGE_ALLOWED),
|
limit_price=price * (1 - context.SLIPPAGE_ALLOWED),
|
||||||
)
|
)
|
||||||
# action = 0
|
action = 0
|
||||||
context.position = None
|
context.position = None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -96,7 +97,7 @@ def _handle_buy_sell_decision(context, data, signal, price):
|
|||||||
amount=buy_amount,
|
amount=buy_amount,
|
||||||
stop=None
|
stop=None
|
||||||
)
|
)
|
||||||
# action = 0
|
action = 0
|
||||||
|
|
||||||
|
|
||||||
def _handle_data_rsi_only(context, data):
|
def _handle_data_rsi_only(context, data):
|
||||||
@@ -114,7 +115,7 @@ def _handle_data_rsi_only(context, data):
|
|||||||
prices = data.history(
|
prices = data.history(
|
||||||
context.asset,
|
context.asset,
|
||||||
fields='price',
|
fields='price',
|
||||||
bar_count=20,
|
bar_count=17,
|
||||||
frequency='30T'
|
frequency='30T'
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -156,7 +157,7 @@ def handle_data(context, data):
|
|||||||
dt = data.current_dt
|
dt = data.current_dt
|
||||||
|
|
||||||
if context.last_bar is None or (
|
if context.last_bar is None or (
|
||||||
context.last_bar + timedelta(minutes=15)) <= dt:
|
context.last_bar + timedelta(minutes=15)) <= dt:
|
||||||
context.last_bar = dt
|
context.last_bar = dt
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
@@ -249,17 +250,27 @@ def analyze(context=None, results=None):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
# run_algorithm(
|
||||||
# Backtest
|
# initialize=initialize,
|
||||||
run_algorithm(
|
# handle_data=handle_data,
|
||||||
capital_base=0.5,
|
# analyze=analyze,
|
||||||
data_frequency='minute',
|
# exchange_name='bittrex',
|
||||||
initialize=initialize,
|
# live=True,
|
||||||
handle_data=handle_data,
|
# algo_namespace=algo_namespace,
|
||||||
analyze=analyze,
|
# base_currency='btc',
|
||||||
exchange_name='poloniex',
|
# live_graph=False
|
||||||
algo_namespace=algo_namespace,
|
# )
|
||||||
base_currency='btc',
|
|
||||||
start=pd.to_datetime('2017-9-1', utc=True),
|
# Backtest
|
||||||
end=pd.to_datetime('2017-10-1', utc=True),
|
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),
|
||||||
|
)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,38 +1,32 @@
|
|||||||
import pandas as pd
|
|
||||||
import talib
|
import talib
|
||||||
from logbook import Logger, INFO
|
import pandas as pd
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
from catalyst import run_algorithm
|
||||||
from catalyst.api import symbol, record
|
from catalyst.api import symbol, record
|
||||||
from catalyst.exchange.utils.stats_utils import get_pretty_stats, \
|
from catalyst.exchange.stats_utils import get_pretty_stats, \
|
||||||
extract_transactions
|
extract_transactions
|
||||||
|
|
||||||
log = Logger('simple_loop', level=INFO)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
log.info('initializing')
|
print('initializing')
|
||||||
context.asset = symbol('eth_btc')
|
context.asset = symbol('neo_eth')
|
||||||
context.base_price = None
|
context.base_price = None
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
log.info('handling bar: {}'.format(data.current_dt))
|
print('handling bar: {}'.format(data.current_dt))
|
||||||
|
|
||||||
price = data.current(context.asset, 'close')
|
price = data.current(context.asset, 'close')
|
||||||
log.info('got price {price}'.format(price=price))
|
print('got price {price}'.format(price=price))
|
||||||
|
|
||||||
prices = data.history(
|
prices = data.history(
|
||||||
context.asset,
|
context.asset,
|
||||||
fields='price',
|
fields='price',
|
||||||
bar_count=20,
|
bar_count=20,
|
||||||
frequency='30T'
|
frequency='15T'
|
||||||
)
|
)
|
||||||
last_traded = prices.index[-1]
|
|
||||||
log.info('last candle date: {}'.format(last_traded))
|
|
||||||
|
|
||||||
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
||||||
log.info('got rsi: {}'.format(rsi))
|
print('got rsi: {}'.format(rsi))
|
||||||
|
|
||||||
# If base_price is not set, we use the current value. This is the
|
# 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.
|
# price at the first bar which we reference to calculate price_change.
|
||||||
@@ -54,7 +48,7 @@ def handle_data(context, data):
|
|||||||
|
|
||||||
def analyze(context, perf):
|
def analyze(context, perf):
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
log.info('the stats: {}'.format(get_pretty_stats(perf)))
|
print('the stats: {}'.format(get_pretty_stats(perf)))
|
||||||
|
|
||||||
# The base currency of the algo exchange
|
# The base currency of the algo exchange
|
||||||
base_currency = context.exchanges.values()[0].base_currency.upper()
|
base_currency = context.exchanges.values()[0].base_currency.upper()
|
||||||
@@ -113,32 +107,25 @@ def analyze(context, perf):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
run_algorithm(
|
||||||
mode = 'backtest'
|
capital_base=250,
|
||||||
|
start=pd.to_datetime('2017-11-1 0:00', utc=True),
|
||||||
if mode == 'backtest':
|
end=pd.to_datetime('2017-11-10 23:59', utc=True),
|
||||||
run_algorithm(
|
data_frequency='daily',
|
||||||
capital_base=1,
|
initialize=initialize,
|
||||||
initialize=initialize,
|
handle_data=handle_data,
|
||||||
handle_data=handle_data,
|
analyze=analyze,
|
||||||
analyze=None,
|
exchange_name='bitfinex',
|
||||||
exchange_name='poloniex',
|
algo_namespace='simple_loop',
|
||||||
algo_namespace='simple_loop',
|
base_currency='usd'
|
||||||
base_currency='eth',
|
)
|
||||||
data_frequency='minute',
|
# run_algorithm(
|
||||||
start=pd.to_datetime('2017-9-1', utc=True),
|
# initialize=initialize,
|
||||||
end=pd.to_datetime('2017-12-1', utc=True),
|
# handle_data=handle_data,
|
||||||
)
|
# analyze=None,
|
||||||
else:
|
# exchange_name='binance',
|
||||||
run_algorithm(
|
# live=True,
|
||||||
capital_base=1,
|
# algo_namespace='simple_loop',
|
||||||
initialize=initialize,
|
# base_currency='eth',
|
||||||
handle_data=handle_data,
|
# live_graph=False,
|
||||||
analyze=None,
|
# )
|
||||||
exchange_name='binance',
|
|
||||||
live=True,
|
|
||||||
algo_namespace='simple_loop',
|
|
||||||
base_currency='eth',
|
|
||||||
live_graph=False,
|
|
||||||
simulate_orders=True
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -2,117 +2,73 @@
|
|||||||
Requires Catalyst version 0.3.0 or above
|
Requires Catalyst version 0.3.0 or above
|
||||||
Tested on Catalyst version 0.3.3
|
Tested on Catalyst version 0.3.3
|
||||||
|
|
||||||
This example aims to provide an easy way for users to learn how to
|
These example aims to provide and easy way for users to learn how to collect data from the different exchanges.
|
||||||
collect data from any given exchange and select a subset of the available
|
You simply need to specify the exchange and the market that you want to focus on.
|
||||||
currency pairs for trading. You simply need to specify the exchange and
|
You will all see how to create a universe and filter it base on the exchange and the market you desire.
|
||||||
the market (base_currency) that you want to focus on. You will then see
|
|
||||||
how to create a universe of assets, and filter it based the market you
|
|
||||||
desire.
|
|
||||||
|
|
||||||
The example prints out the closing price of all the pairs for a given
|
The example prints out the closing price of all the pairs for a given market-exchange every 30 minutes.
|
||||||
market in a given exchange every 30 minutes. The example also contains
|
The example also contains the ohlcv minute data for the past seven days which could be used to create indicators
|
||||||
the OHLCV data with minute-resolution for the past seven days which
|
Use this as the backbone to create your own trading strategies.
|
||||||
could be used to create indicators. Use this code as the backbone to
|
|
||||||
create your own trading strategy.
|
|
||||||
|
|
||||||
The lookback_date variable is used to ensure data for a coin existed on
|
|
||||||
the lookback period specified.
|
|
||||||
|
|
||||||
To run, execute the following two commands in a terminal (inside catalyst
|
|
||||||
environment). The first one retrieves all the pricing data needed for this
|
|
||||||
script to run (only needs to be run once), and the second one executes this
|
|
||||||
script with the parameters specified in the run_algorithm() call at the end
|
|
||||||
of the file:
|
|
||||||
|
|
||||||
catalyst ingest-exchange -x bitfinex -f minute
|
|
||||||
|
|
||||||
python simple_universe.py
|
|
||||||
|
|
||||||
|
Variables lookback date and date are used to ensure data for a coin existed on the lookback period specified.
|
||||||
"""
|
"""
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from datetime import timedelta
|
||||||
from catalyst import run_algorithm
|
from catalyst import run_algorithm
|
||||||
from catalyst.api import (symbols, )
|
from catalyst.exchange.exchange_utils import get_exchange_symbols
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_symbols
|
|
||||||
|
from catalyst.api import (
|
||||||
|
symbols,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.i = -1 # minute counter
|
context.i = -1 # counts the minutes
|
||||||
context.exchange = context.exchanges.values()[0].name.lower()
|
context.exchange = context.exchanges.values()[0].name.lower() # exchange name
|
||||||
context.base_currency = context.exchanges.values()[0].base_currency.lower()
|
context.base_currency = context.exchanges.values()[0].base_currency.lower() # market base currency
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
context.i += 1
|
context.i += 1
|
||||||
lookback_days = 7 # 7 days
|
lookback_days = 7 # 7 days
|
||||||
|
|
||||||
# current date & time in each iteration formatted into a string
|
# current date formatted into a string
|
||||||
now = data.current_dt
|
today = data.current_dt
|
||||||
date, time = now.strftime('%Y-%m-%d %H:%M:%S').split(' ')
|
date, time = today.strftime('%Y-%m-%d %H:%M:%S').split(' ')
|
||||||
lookback_date = now - timedelta(days=lookback_days)
|
lookback_date = today - timedelta(days=lookback_days) # subtract the amount of days specified in lookback
|
||||||
# keep only the date as a string, discard the time
|
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[0] # get only the date as a string
|
||||||
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[0]
|
|
||||||
|
|
||||||
one_day_in_minutes = 1440 # 60 * 24 assumes data_frequency='minute'
|
# update universe everyday
|
||||||
# update universe everyday at midnight
|
new_day = 60 * 24 # assuming data_frequency='minute'
|
||||||
if not context.i % one_day_in_minutes:
|
if not context.i % new_day:
|
||||||
context.universe = universe(context, lookback_date, date)
|
context.universe = universe(context, lookback_date, date)
|
||||||
|
|
||||||
# get data every 30 minutes
|
# get data every 30 minutes
|
||||||
minutes = 30
|
minutes = 30
|
||||||
|
one_day_in_minutes = 1440 # 1440 assumes data_frequency='minute'
|
||||||
# get lookback_days of history data: that is 'lookback' number of bins
|
lookback = one_day_in_minutes / minutes * lookback_days # get N lookback_days of history data
|
||||||
lookback = one_day_in_minutes / minutes * lookback_days
|
|
||||||
if not context.i % minutes and context.universe:
|
if not context.i % minutes and context.universe:
|
||||||
# we iterate for every pair in the current universe
|
# we iterate for every pair in the current universe
|
||||||
for coin in context.coins:
|
for coin in context.coins:
|
||||||
pair = str(coin.symbol)
|
pair = str(coin.symbol)
|
||||||
|
|
||||||
# Get 30 minute interval OHLCV data. This is the standard data
|
# 30 minute interval ohlcv data (the standard data required for candlestick or indicators/signals)
|
||||||
# required for candlestick or indicators/signals. Return Pandas
|
# 30T means 30 minutes re-sampling of one minute data. change to your desire time interval.
|
||||||
# DataFrames. 30T means 30-minute re-sampling of one minute data.
|
opened = fill(data.history(coin, 'open', bar_count=lookback, frequency='30T')).values
|
||||||
# Adjust it to your desired time interval as needed.
|
high = fill(data.history(coin, 'high', bar_count=lookback, frequency='30T')).values
|
||||||
opened = fill(data.history(coin,
|
low = fill(data.history(coin, 'low', bar_count=lookback, frequency='30T')).values
|
||||||
'open',
|
close = fill(data.history(coin, 'price', bar_count=lookback, frequency='30T')).values
|
||||||
bar_count=lookback,
|
volume = fill(data.history(coin, 'volume', bar_count=lookback, frequency='30T')).values
|
||||||
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 last value in the set, which is the equivalent
|
# close[-1] is the equivalent to current price
|
||||||
# to current price (as in the most recent value)
|
|
||||||
# displays the minute price for each pair every 30 minutes
|
# displays the minute price for each pair every 30 minutes
|
||||||
print('{now}: {pair} -\tO:{o},\tH:{h},\tL:{c},\tC{c},'
|
print(today, pair, opened[-1], high[-1], low[-1], close[-1], volume[-1])
|
||||||
'\tV:{v}'.format(
|
|
||||||
now=now,
|
|
||||||
pair=pair,
|
|
||||||
o=opened[-1],
|
|
||||||
h=high[-1],
|
|
||||||
l=low[-1],
|
|
||||||
c=close[-1],
|
|
||||||
v=volume[-1],
|
|
||||||
))
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# ----------------------------------------------------------------------------------------------------------
|
||||||
# --------------- Insert Your Strategy Here -------------------
|
# -------------------------------------- Insert Your Strategy Here -----------------------------------------
|
||||||
# -------------------------------------------------------------
|
# ----------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def analyze(context=None, results=None):
|
def analyze(context=None, results=None):
|
||||||
@@ -122,24 +78,23 @@ 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):
|
||||||
# get all the pairs for the given exchange
|
json_symbols = get_exchange_symbols(context.exchange) # get all the pairs for the exchange
|
||||||
json_symbols = get_exchange_symbols(context.exchange)
|
universe_df = pd.DataFrame.from_dict(json_symbols).transpose().astype(str) # convert into a dataframe
|
||||||
# convert into a DataFrame for easier processing
|
universe_df['base_currency'] = universe_df.apply(lambda row: row.symbol.split('_')[1],
|
||||||
df = pd.DataFrame.from_dict(json_symbols).transpose().astype(str)
|
axis=1)
|
||||||
df['base_currency'] = df.apply(lambda row: row.symbol.split('_')[1],
|
universe_df['market_currency'] = universe_df.apply(lambda row: row.symbol.split('_')[0],
|
||||||
axis=1)
|
axis=1)
|
||||||
df['market_currency'] = df.apply(lambda row: row.symbol.split('_')[0],
|
|
||||||
axis=1)
|
|
||||||
|
|
||||||
# Filter all the pairs to get only the ones for a given base_currency
|
# Filter all the exchange pairs to only the ones for a give base currency
|
||||||
df = df[df['base_currency'] == context.base_currency]
|
universe_df = universe_df[universe_df['base_currency'] == context.base_currency]
|
||||||
|
|
||||||
# Filter all pairs to ensure that pair existed in the current date range
|
# Filter all the pairs to ensure that pair existed in the current date range
|
||||||
df = df[df.start_date < lookback_date]
|
universe_df = universe_df[universe_df.start_date < lookback_date]
|
||||||
df = df[df.end_daily >= current_date]
|
universe_df = universe_df[universe_df.end_daily >= current_date]
|
||||||
context.coins = symbols(*df.symbol) # convert all the pairs to symbols
|
context.coins = symbols(*universe_df.symbol) # convert all the pairs to symbols
|
||||||
|
|
||||||
return df.symbol.tolist()
|
# print(universe_df.symbol.tolist())
|
||||||
|
return universe_df.symbol.tolist()
|
||||||
|
|
||||||
|
|
||||||
# Replace all NA, NAN or infinite values with its nearest value
|
# Replace all NA, NAN or infinite values with its nearest value
|
||||||
@@ -147,9 +102,7 @@ def fill(series):
|
|||||||
if isinstance(series, pd.Series):
|
if isinstance(series, pd.Series):
|
||||||
return series.replace([np.inf, -np.inf], np.nan).ffill().bfill()
|
return series.replace([np.inf, -np.inf], np.nan).ffill().bfill()
|
||||||
elif isinstance(series, np.ndarray):
|
elif isinstance(series, np.ndarray):
|
||||||
return pd.Series(series).replace(
|
return pd.Series(series).replace([np.inf, -np.inf], np.nan).ffill().bfill().values
|
||||||
[np.inf, -np.inf], np.nan
|
|
||||||
).ffill().bfill().values
|
|
||||||
else:
|
else:
|
||||||
return series
|
return series
|
||||||
|
|
||||||
@@ -159,13 +112,18 @@ if __name__ == '__main__':
|
|||||||
end_date = pd.to_datetime('2017-11-13', 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=100.0, # amount of base_currency
|
capital_base=100.0, # amount of base_currency, not always in dollars unless usd
|
||||||
initialize=initialize,
|
initialize=initialize,
|
||||||
handle_data=handle_data,
|
handle_data=handle_data,
|
||||||
analyze=analyze,
|
analyze=analyze,
|
||||||
exchange_name='poloniex',
|
exchange_name='bitfinex',
|
||||||
data_frequency='minute',
|
data_frequency='minute',
|
||||||
base_currency='btc',
|
base_currency='btc',
|
||||||
live=False,
|
live=False,
|
||||||
live_graph=False,
|
live_graph=False,
|
||||||
algo_namespace='simple_universe')
|
algo_namespace='simple_universe')
|
||||||
|
|
||||||
|
"""
|
||||||
|
Run in Terminal (inside catalyst environment):
|
||||||
|
python simple_universe.py
|
||||||
|
"""
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
# Run Command
|
# Run Command
|
||||||
# catalyst run --start 2017-1-1 --end 2017-11-1 -o talib_simple.pickle \
|
# catalyst run --start 2017-1-1 --end 2017-11-1 -o talib_simple.pickle -f talib_simple.py -x poloniex
|
||||||
# -f talib_simple.py -x poloniex
|
|
||||||
#
|
#
|
||||||
# Description
|
# Description
|
||||||
# Simple TALib Example showing how to use various indicators
|
# Simple TALib Example showing how to use various indicators in you strategy
|
||||||
# in you strategy. Based loosly on
|
# Based loosly on https://github.com/mellertson/talib-macd-example/blob/master/talib-macd-matplotlib-example.py
|
||||||
# https://github.com/mellertson/talib-macd-example/blob/master/talib-macd-matplotlib-example.py
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -23,7 +21,7 @@ from catalyst.api import (
|
|||||||
order_target_percent,
|
order_target_percent,
|
||||||
symbol,
|
symbol,
|
||||||
)
|
)
|
||||||
from catalyst.exchange.utils.stats_utils import get_pretty_stats
|
from catalyst.exchange.stats_utils import get_pretty_stats
|
||||||
|
|
||||||
algo_namespace = 'talib_sample'
|
algo_namespace = 'talib_sample'
|
||||||
log = Logger(algo_namespace)
|
log = Logger(algo_namespace)
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('AssetFinderExchange', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
|
class AssetFinderExchange(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._asset_cache = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sids(self):
|
||||||
|
"""
|
||||||
|
This seems to be used to pre-fetch assets.
|
||||||
|
I don't think that we need this for live-trading.
|
||||||
|
Leaving the list empty.
|
||||||
|
"""
|
||||||
|
return list()
|
||||||
|
|
||||||
|
def retrieve_all(self, sids, default_none=False):
|
||||||
|
"""
|
||||||
|
Retrieve all assets in `sids`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
sids : iterable of int
|
||||||
|
Assets to retrieve.
|
||||||
|
default_none : bool
|
||||||
|
If True, return None for failed lookups.
|
||||||
|
If False, raise `SidsNotFound`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
assets : list[Asset or None]
|
||||||
|
A list of the same length as `sids` containing Assets (or Nones)
|
||||||
|
corresponding to the requested sids.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
SidsNotFound
|
||||||
|
When a requested sid is not found and default_none=False.
|
||||||
|
"""
|
||||||
|
# for sid in sids:
|
||||||
|
# if sid in self._asset_cache:
|
||||||
|
# log.debug('got asset from cache: {}'.format(sid))
|
||||||
|
# else:
|
||||||
|
# log.debug('fetching asset: {}'.format(sid))
|
||||||
|
return list()
|
||||||
|
|
||||||
|
def lookup_symbol(self, symbol, exchange, data_frequency=None,
|
||||||
|
as_of_date=None, fuzzy=False):
|
||||||
|
"""Lookup an asset by symbol.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
symbol : str
|
||||||
|
The ticker symbol to resolve.
|
||||||
|
as_of_date : datetime or None
|
||||||
|
Look up the last owner of this symbol as of this datetime.
|
||||||
|
If ``as_of_date`` is None, then this can only resolve the equity
|
||||||
|
if exactly one equity has ever owned the ticker.
|
||||||
|
fuzzy : bool, optional
|
||||||
|
Should fuzzy symbol matching be used? Fuzzy symbol matching
|
||||||
|
attempts to resolve differences in representations for
|
||||||
|
shareclasses. For example, some people may represent the ``A``
|
||||||
|
shareclass of ``BRK`` as ``BRK.A``, where others could write
|
||||||
|
``BRK_A``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
equity : Asset
|
||||||
|
The equity that held ``symbol`` on the given ``as_of_date``, or the
|
||||||
|
only equity to hold ``symbol`` if ``as_of_date`` is None.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
SymbolNotFound
|
||||||
|
Raised when no equity has ever held the given symbol.
|
||||||
|
MultipleSymbolsFound
|
||||||
|
Raised when no ``as_of_date`` is given and more than one equity
|
||||||
|
has held ``symbol``. This is also raised when ``fuzzy=True`` and
|
||||||
|
there are multiple candidates for the given ``symbol`` on the
|
||||||
|
``as_of_date``.
|
||||||
|
"""
|
||||||
|
log.debug('looking up symbol: {} {}'.format(symbol, exchange.name))
|
||||||
|
|
||||||
|
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:
|
||||||
|
return self._asset_cache[key]
|
||||||
|
else:
|
||||||
|
asset = exchange.get_asset(symbol, data_frequency)
|
||||||
|
self._asset_cache[key] = asset
|
||||||
|
return asset
|
||||||
@@ -0,0 +1,705 @@
|
|||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytz
|
||||||
|
import requests
|
||||||
|
import six
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst.exchange.exchange import Exchange
|
||||||
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
|
from catalyst.exchange.exchange_errors import (
|
||||||
|
ExchangeRequestError,
|
||||||
|
InvalidHistoryFrequencyError,
|
||||||
|
InvalidOrderStyle, OrderCancelError)
|
||||||
|
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
|
||||||
|
ExchangeStopLimitOrder, ExchangeStopOrder
|
||||||
|
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename, \
|
||||||
|
download_exchange_symbols, get_symbols_string
|
||||||
|
from catalyst.finance.order import Order, ORDER_STATUS
|
||||||
|
from catalyst.protocol import Account
|
||||||
|
|
||||||
|
# Trying to account for REST api instability
|
||||||
|
# https://stackoverflow.com/questions/15431044/can-i-set-max-retries-for-requests-request
|
||||||
|
requests.adapters.DEFAULT_RETRIES = 20
|
||||||
|
|
||||||
|
BITFINEX_URL = 'https://api.bitfinex.com'
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('Bitfinex', level=LOG_LEVEL)
|
||||||
|
warning_logger = Logger('AlgoWarning')
|
||||||
|
|
||||||
|
|
||||||
|
class Bitfinex(Exchange):
|
||||||
|
def __init__(self, key, secret, base_currency, portfolio=None):
|
||||||
|
self.url = BITFINEX_URL
|
||||||
|
self.key = key
|
||||||
|
self.secret = secret.encode('UTF-8')
|
||||||
|
self.name = 'bitfinex'
|
||||||
|
self.color = 'green'
|
||||||
|
|
||||||
|
self.assets = dict()
|
||||||
|
self.load_assets()
|
||||||
|
|
||||||
|
self.local_assets = dict()
|
||||||
|
self.load_assets(is_local=True)
|
||||||
|
|
||||||
|
self.base_currency = base_currency
|
||||||
|
self._portfolio = portfolio
|
||||||
|
self.minute_writer = None
|
||||||
|
self.minute_reader = None
|
||||||
|
|
||||||
|
# The candle limit for each request
|
||||||
|
self.num_candles_limit = 1000
|
||||||
|
|
||||||
|
# Max is 90 but playing it safe
|
||||||
|
# https://www.bitfinex.com/posts/188
|
||||||
|
self.max_requests_per_minute = 80
|
||||||
|
self.request_cpt = dict()
|
||||||
|
|
||||||
|
self.bundle = ExchangeBundle(self.name)
|
||||||
|
|
||||||
|
def _request(self, operation, data, version='v1'):
|
||||||
|
payload_object = {
|
||||||
|
'request': '/{}/{}'.format(version, operation),
|
||||||
|
'nonce': '{0:f}'.format(time.time() * 1000000),
|
||||||
|
# convert to string
|
||||||
|
'options': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
payload_dict = payload_object
|
||||||
|
else:
|
||||||
|
payload_dict = payload_object.copy()
|
||||||
|
payload_dict.update(data)
|
||||||
|
|
||||||
|
payload_json = json.dumps(payload_dict)
|
||||||
|
if six.PY3:
|
||||||
|
payload = base64.b64encode(bytes(payload_json, 'utf-8'))
|
||||||
|
else:
|
||||||
|
payload = base64.b64encode(payload_json)
|
||||||
|
|
||||||
|
m = hmac.new(self.secret, payload, hashlib.sha384)
|
||||||
|
m = m.hexdigest()
|
||||||
|
|
||||||
|
# headers
|
||||||
|
headers = {
|
||||||
|
'X-BFX-APIKEY': self.key,
|
||||||
|
'X-BFX-PAYLOAD': payload,
|
||||||
|
'X-BFX-SIGNATURE': m
|
||||||
|
}
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
request = requests.get(
|
||||||
|
'{url}/{version}/{operation}'.format(
|
||||||
|
url=self.url,
|
||||||
|
version=version,
|
||||||
|
operation=operation
|
||||||
|
), data={},
|
||||||
|
headers=headers)
|
||||||
|
else:
|
||||||
|
request = requests.post(
|
||||||
|
'{url}/{version}/{operation}'.format(
|
||||||
|
url=self.url,
|
||||||
|
version=version,
|
||||||
|
operation=operation
|
||||||
|
),
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
|
return request
|
||||||
|
|
||||||
|
def _get_v2_symbol(self, asset):
|
||||||
|
pair = asset.symbol.split('_')
|
||||||
|
symbol = 't' + pair[0].upper() + pair[1].upper()
|
||||||
|
return symbol
|
||||||
|
|
||||||
|
def _get_v2_symbols(self, assets):
|
||||||
|
"""
|
||||||
|
Workaround to support Bitfinex v2
|
||||||
|
TODO: Might require a separate asset dictionary
|
||||||
|
|
||||||
|
:param assets:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
v2_symbols = []
|
||||||
|
for asset in assets:
|
||||||
|
v2_symbols.append(self._get_v2_symbol(asset))
|
||||||
|
|
||||||
|
return v2_symbols
|
||||||
|
|
||||||
|
def _create_order(self, order_status):
|
||||||
|
"""
|
||||||
|
Create a Catalyst order object from a Bitfinex order dictionary
|
||||||
|
:param order_status:
|
||||||
|
:return: Order
|
||||||
|
"""
|
||||||
|
if order_status['is_cancelled']:
|
||||||
|
status = ORDER_STATUS.CANCELLED
|
||||||
|
elif not order_status['is_live']:
|
||||||
|
log.info('found executed order {}'.format(order_status))
|
||||||
|
status = ORDER_STATUS.FILLED
|
||||||
|
else:
|
||||||
|
status = ORDER_STATUS.OPEN
|
||||||
|
|
||||||
|
amount = float(order_status['original_amount'])
|
||||||
|
filled = float(order_status['executed_amount'])
|
||||||
|
|
||||||
|
if order_status['side'] == 'sell':
|
||||||
|
amount = -amount
|
||||||
|
filled = -filled
|
||||||
|
|
||||||
|
price = float(order_status['price'])
|
||||||
|
order_type = order_status['type']
|
||||||
|
|
||||||
|
stop_price = None
|
||||||
|
limit_price = None
|
||||||
|
|
||||||
|
# TODO: is this comprehensive enough?
|
||||||
|
if order_type.endswith('limit'):
|
||||||
|
limit_price = price
|
||||||
|
elif order_type.endswith('stop'):
|
||||||
|
stop_price = price
|
||||||
|
|
||||||
|
executed_price = float(order_status['avg_execution_price'])
|
||||||
|
|
||||||
|
# TODO: bitfinex does not specify comission. I could calculate it but not sure if it's worth it.
|
||||||
|
commission = None
|
||||||
|
|
||||||
|
date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp']))
|
||||||
|
date = pytz.utc.localize(date)
|
||||||
|
order = Order(
|
||||||
|
dt=date,
|
||||||
|
asset=self.assets[order_status['symbol']],
|
||||||
|
amount=amount,
|
||||||
|
stop=stop_price,
|
||||||
|
limit=limit_price,
|
||||||
|
filled=filled,
|
||||||
|
id=str(order_status['id']),
|
||||||
|
commission=commission
|
||||||
|
)
|
||||||
|
order.status = status
|
||||||
|
|
||||||
|
return order, executed_price
|
||||||
|
|
||||||
|
def get_balances(self):
|
||||||
|
log.debug('retrieving wallets balances')
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
response = self._request('balances', None)
|
||||||
|
balances = response.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'message' in balances:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='unable to fetch balance {}'.format(balances['message'])
|
||||||
|
)
|
||||||
|
|
||||||
|
std_balances = dict()
|
||||||
|
for balance in balances:
|
||||||
|
currency = balance['currency'].lower()
|
||||||
|
std_balances[currency] = float(balance['available'])
|
||||||
|
|
||||||
|
return std_balances
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account(self):
|
||||||
|
account = Account()
|
||||||
|
|
||||||
|
account.settled_cash = None
|
||||||
|
account.accrued_interest = None
|
||||||
|
account.buying_power = None
|
||||||
|
account.equity_with_loan = None
|
||||||
|
account.total_positions_value = None
|
||||||
|
account.total_positions_exposure = None
|
||||||
|
account.regt_equity = None
|
||||||
|
account.regt_margin = None
|
||||||
|
account.initial_margin_requirement = None
|
||||||
|
account.maintenance_margin_requirement = None
|
||||||
|
account.available_funds = None
|
||||||
|
account.excess_liquidity = None
|
||||||
|
account.cushion = None
|
||||||
|
account.day_trades_remaining = None
|
||||||
|
account.leverage = None
|
||||||
|
account.net_leverage = None
|
||||||
|
account.net_liquidation = None
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_skew(self):
|
||||||
|
# TODO: research the time skew conditions
|
||||||
|
return pd.Timedelta('0s')
|
||||||
|
|
||||||
|
def get_account(self):
|
||||||
|
# TODO: fetch account data and keep in cache
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_candles(self, freq, assets, bar_count=None,
|
||||||
|
start_dt=None, end_dt=None):
|
||||||
|
"""
|
||||||
|
Retrieve OHLVC candles from Bitfinex
|
||||||
|
|
||||||
|
:param data_frequency:
|
||||||
|
:param assets:
|
||||||
|
:param bar_count:
|
||||||
|
:return:
|
||||||
|
|
||||||
|
Available Frequencies
|
||||||
|
---------------------
|
||||||
|
'1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D', '14D',
|
||||||
|
'1M'
|
||||||
|
"""
|
||||||
|
log.debug(
|
||||||
|
'retrieving {bars} {freq} candles on {exchange} from '
|
||||||
|
'{end_dt} for markets {symbols}, '.format(
|
||||||
|
bars=bar_count,
|
||||||
|
freq=freq,
|
||||||
|
exchange=self.name,
|
||||||
|
end_dt=end_dt,
|
||||||
|
symbols=get_symbols_string(assets)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
allowed_frequencies = ['1T', '5T', '15T', '30T', '60T', '180T',
|
||||||
|
'360T', '720T', '1D', '7D', '14D', '30D']
|
||||||
|
if freq not in allowed_frequencies:
|
||||||
|
raise InvalidHistoryFrequencyError(frequency=freq)
|
||||||
|
|
||||||
|
freq_match = re.match(r'([0-9].*)(T|H|D)', freq, re.M | re.I)
|
||||||
|
if freq_match:
|
||||||
|
number = int(freq_match.group(1))
|
||||||
|
unit = freq_match.group(2)
|
||||||
|
|
||||||
|
if unit == 'T':
|
||||||
|
if number in [60, 180, 360, 720]:
|
||||||
|
number = number / 60
|
||||||
|
converted_unit = 'h'
|
||||||
|
else:
|
||||||
|
converted_unit = 'm'
|
||||||
|
else:
|
||||||
|
converted_unit = unit
|
||||||
|
|
||||||
|
frequency = '{}{}'.format(number, converted_unit)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise InvalidHistoryFrequencyError(frequency=freq)
|
||||||
|
|
||||||
|
# Making sure that assets are iterable
|
||||||
|
asset_list = [assets] if isinstance(assets, TradingPair) else assets
|
||||||
|
ohlc_map = dict()
|
||||||
|
for asset in asset_list:
|
||||||
|
symbol = self._get_v2_symbol(asset)
|
||||||
|
url = '{url}/v2/candles/trade:{frequency}:{symbol}'.format(
|
||||||
|
url=self.url,
|
||||||
|
frequency=frequency,
|
||||||
|
symbol=symbol
|
||||||
|
)
|
||||||
|
|
||||||
|
if bar_count:
|
||||||
|
is_list = True
|
||||||
|
url += '/hist?limit={}'.format(int(bar_count))
|
||||||
|
|
||||||
|
def get_ms(date):
|
||||||
|
epoch = datetime.datetime.utcfromtimestamp(0)
|
||||||
|
epoch = epoch.replace(tzinfo=pytz.UTC)
|
||||||
|
|
||||||
|
return (date - epoch).total_seconds() * 1000.0
|
||||||
|
|
||||||
|
if start_dt is not None:
|
||||||
|
start_ms = get_ms(start_dt)
|
||||||
|
url += '&start={0:f}'.format(start_ms)
|
||||||
|
|
||||||
|
if end_dt is not None:
|
||||||
|
end_ms = get_ms(end_dt)
|
||||||
|
url += '&end={0:f}'.format(end_ms)
|
||||||
|
|
||||||
|
else:
|
||||||
|
is_list = False
|
||||||
|
url += '/last'
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
response = requests.get(url)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'error' in response.content:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='Unable to retrieve candles: {}'.format(
|
||||||
|
response.content)
|
||||||
|
)
|
||||||
|
|
||||||
|
candles = response.json()
|
||||||
|
|
||||||
|
def ohlc_from_candle(candle):
|
||||||
|
last_traded = pd.Timestamp.utcfromtimestamp(
|
||||||
|
candle[0] / 1000.0)
|
||||||
|
last_traded = last_traded.replace(tzinfo=pytz.UTC)
|
||||||
|
ohlc = dict(
|
||||||
|
open=np.float64(candle[1]),
|
||||||
|
high=np.float64(candle[3]),
|
||||||
|
low=np.float64(candle[4]),
|
||||||
|
close=np.float64(candle[2]),
|
||||||
|
volume=np.float64(candle[5]),
|
||||||
|
price=np.float64(candle[2]),
|
||||||
|
last_traded=last_traded
|
||||||
|
)
|
||||||
|
return ohlc
|
||||||
|
|
||||||
|
if is_list:
|
||||||
|
ohlc_bars = []
|
||||||
|
# We can to list candles from old to new
|
||||||
|
for candle in reversed(candles):
|
||||||
|
ohlc = ohlc_from_candle(candle)
|
||||||
|
ohlc_bars.append(ohlc)
|
||||||
|
|
||||||
|
ohlc_map[asset] = ohlc_bars
|
||||||
|
|
||||||
|
else:
|
||||||
|
ohlc = ohlc_from_candle(candles)
|
||||||
|
ohlc_map[asset] = ohlc
|
||||||
|
|
||||||
|
return ohlc_map[assets] \
|
||||||
|
if isinstance(assets, TradingPair) else ohlc_map
|
||||||
|
|
||||||
|
def create_order(self, asset, amount, is_buy, style):
|
||||||
|
"""
|
||||||
|
Creating order on the exchange.
|
||||||
|
|
||||||
|
:param asset:
|
||||||
|
:param amount:
|
||||||
|
:param is_buy:
|
||||||
|
:param style:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
exchange_symbol = self.get_symbol(asset)
|
||||||
|
if isinstance(style, ExchangeLimitOrder) \
|
||||||
|
or isinstance(style, ExchangeStopLimitOrder):
|
||||||
|
price = style.get_limit_price(is_buy)
|
||||||
|
order_type = 'limit'
|
||||||
|
|
||||||
|
elif isinstance(style, ExchangeStopOrder):
|
||||||
|
price = style.get_stop_price(is_buy)
|
||||||
|
order_type = 'stop'
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise InvalidOrderStyle(exchange=self.name,
|
||||||
|
style=style.__class__.__name__)
|
||||||
|
|
||||||
|
req = dict(
|
||||||
|
symbol=exchange_symbol,
|
||||||
|
amount=str(float(abs(amount))),
|
||||||
|
price="{:.20f}".format(float(price)),
|
||||||
|
side='buy' if is_buy else 'sell',
|
||||||
|
type='exchange ' + order_type, # TODO: support margin trades
|
||||||
|
exchange=self.name,
|
||||||
|
is_hidden=False,
|
||||||
|
is_postonly=False,
|
||||||
|
use_all_available=0,
|
||||||
|
ocoorder=False,
|
||||||
|
buy_price_oco=0,
|
||||||
|
sell_price_oco=0
|
||||||
|
)
|
||||||
|
|
||||||
|
date = pd.Timestamp.utcnow()
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
response = self._request('order/new', req)
|
||||||
|
order_status = response.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'message' in order_status:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='unable to create Bitfinex order {}'.format(
|
||||||
|
order_status['message'])
|
||||||
|
)
|
||||||
|
|
||||||
|
order_id = str(order_status['id'])
|
||||||
|
order = Order(
|
||||||
|
dt=date,
|
||||||
|
asset=asset,
|
||||||
|
amount=amount,
|
||||||
|
stop=style.get_stop_price(is_buy),
|
||||||
|
limit=style.get_limit_price(is_buy),
|
||||||
|
id=order_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
def get_open_orders(self, asset=None):
|
||||||
|
"""Retrieve all of the current open orders.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asset : Asset
|
||||||
|
If passed and not None, return only the open orders for the given
|
||||||
|
asset instead of all open orders.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
open_orders : dict[list[Order]] or list[Order]
|
||||||
|
If no asset is passed this will return a dict mapping Assets
|
||||||
|
to a list containing all the open orders for the asset.
|
||||||
|
If an asset is passed then this will return a list of the open
|
||||||
|
orders for this asset.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
response = self._request('orders', None)
|
||||||
|
order_statuses = response.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'message' in order_statuses:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='Unable to retrieve open orders: {}'.format(
|
||||||
|
order_statuses['message'])
|
||||||
|
)
|
||||||
|
|
||||||
|
orders = []
|
||||||
|
for order_status in order_statuses:
|
||||||
|
order, executed_price = self._create_order(order_status)
|
||||||
|
if asset is None or asset == order.sid:
|
||||||
|
orders.append(order)
|
||||||
|
|
||||||
|
return orders
|
||||||
|
|
||||||
|
def get_order(self, order_id):
|
||||||
|
"""Lookup an order based on the order id returned from one of the
|
||||||
|
order functions.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
order_id : str
|
||||||
|
The unique identifier for the order.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
order : Order
|
||||||
|
The order object.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
response = self._request(
|
||||||
|
'order/status', {'order_id': int(order_id)})
|
||||||
|
order_status = response.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'message' in order_status:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='Unable to retrieve order status: {}'.format(
|
||||||
|
order_status['message'])
|
||||||
|
)
|
||||||
|
return self._create_order(order_status)
|
||||||
|
|
||||||
|
def cancel_order(self, order_param):
|
||||||
|
"""Cancel an open order.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
order_param : str or Order
|
||||||
|
The order_id or order object to cancel.
|
||||||
|
"""
|
||||||
|
order_id = order_param.id \
|
||||||
|
if isinstance(order_param, Order) else order_param
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
response = self._request('order/cancel', {'order_id': order_id})
|
||||||
|
status = response.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'message' in status:
|
||||||
|
raise OrderCancelError(
|
||||||
|
order_id=order_id,
|
||||||
|
exchange=self.name,
|
||||||
|
error=status['message']
|
||||||
|
)
|
||||||
|
|
||||||
|
def tickers(self, assets):
|
||||||
|
"""
|
||||||
|
Fetch ticket data for assets
|
||||||
|
https://docs.bitfinex.com/v2/reference#rest-public-tickers
|
||||||
|
|
||||||
|
:param assets:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
symbols = self._get_v2_symbols(assets)
|
||||||
|
log.debug('fetching tickers {}'.format(symbols))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
response = requests.get(
|
||||||
|
'{url}/v2/tickers?symbols={symbols}'.format(
|
||||||
|
url=self.url,
|
||||||
|
symbols=','.join(symbols),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'error' in response.content:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='Unable to retrieve tickers: {}'.format(
|
||||||
|
response.content)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
tickers = response.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
ticks = dict()
|
||||||
|
for index, ticker in enumerate(tickers):
|
||||||
|
if not len(ticker) == 11:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='Invalid ticker in response: {}'.format(ticker)
|
||||||
|
)
|
||||||
|
|
||||||
|
ticks[assets[index]] = dict(
|
||||||
|
timestamp=pd.Timestamp.utcnow(),
|
||||||
|
bid=ticker[1],
|
||||||
|
ask=ticker[3],
|
||||||
|
last_price=ticker[7],
|
||||||
|
low=ticker[10],
|
||||||
|
high=ticker[9],
|
||||||
|
volume=ticker[8],
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug('got tickers {}'.format(ticks))
|
||||||
|
return ticks
|
||||||
|
|
||||||
|
def generate_symbols_json(self, filename=None, source_dates=False):
|
||||||
|
symbol_map = {}
|
||||||
|
|
||||||
|
if not source_dates:
|
||||||
|
fn, r = download_exchange_symbols(self.name)
|
||||||
|
with open(fn) as data_file:
|
||||||
|
cached_symbols = json.load(data_file)
|
||||||
|
|
||||||
|
response = self._request('symbols', None)
|
||||||
|
|
||||||
|
for symbol in response.json():
|
||||||
|
if (source_dates):
|
||||||
|
start_date = self.get_symbol_start_date(symbol)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
start_date = cached_symbols[symbol]['start_date']
|
||||||
|
except KeyError as e:
|
||||||
|
start_date = time.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_daily = cached_symbols[symbol]['end_daily']
|
||||||
|
except KeyError as e:
|
||||||
|
end_daily = 'N/A'
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_minute = cached_symbols[symbol]['end_minute']
|
||||||
|
except KeyError as e:
|
||||||
|
end_minute = 'N/A'
|
||||||
|
|
||||||
|
symbol_map[symbol] = dict(
|
||||||
|
symbol=symbol[:-3] + '_' + symbol[-3:],
|
||||||
|
start_date=start_date,
|
||||||
|
end_daily=end_daily,
|
||||||
|
end_minute=end_minute,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (filename is None):
|
||||||
|
filename = get_exchange_symbols_filename(self.name)
|
||||||
|
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(symbol_map, f, sort_keys=True, indent=2,
|
||||||
|
separators=(',', ':'))
|
||||||
|
|
||||||
|
def get_symbol_start_date(self, symbol):
|
||||||
|
|
||||||
|
print(symbol)
|
||||||
|
symbol_v2 = 't' + symbol.upper()
|
||||||
|
|
||||||
|
"""
|
||||||
|
For each symbol we retrieve candles with Monhtly resolution
|
||||||
|
We get the first month, and query again with daily resolution
|
||||||
|
around that date, and we get the first date
|
||||||
|
"""
|
||||||
|
url = '{url}/v2/candles/trade:1M:{symbol}/hist'.format(
|
||||||
|
url=self.url,
|
||||||
|
symbol=symbol_v2
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
response = requests.get(url)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
"""
|
||||||
|
If we don't get any data back for our monthly-resolution query
|
||||||
|
it means that symbol started trading less than a month ago, so
|
||||||
|
arbitrarily set the ref. date to 15 days ago to be safe with
|
||||||
|
+/- 31 days
|
||||||
|
"""
|
||||||
|
if (len(response.json())):
|
||||||
|
startmonth = response.json()[-1][0]
|
||||||
|
else:
|
||||||
|
startmonth = int((time.time() - 15 * 24 * 3600) * 1000)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Query again with daily resolution setting the start and end around
|
||||||
|
the startmonth we got above. Avoid end dates greater than now: time.time()
|
||||||
|
"""
|
||||||
|
url = '{url}/v2/candles/trade:1D:{symbol}/hist?start={start}&end={end}'.format(
|
||||||
|
url=self.url,
|
||||||
|
symbol=symbol_v2,
|
||||||
|
start=startmonth - 3600 * 24 * 31 * 1000,
|
||||||
|
end=min(startmonth + 3600 * 24 * 31 * 1000,
|
||||||
|
int(time.time() * 1000))
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
response = requests.get(url)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
return time.strftime('%Y-%m-%d',
|
||||||
|
time.gmtime(int(response.json()[-1][0] / 1000)))
|
||||||
|
|
||||||
|
def get_orderbook(self, asset, order_type='all', limit=100):
|
||||||
|
exchange_symbol = asset.exchange_symbol
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
# TODO: implement limit
|
||||||
|
response = self._request(
|
||||||
|
'book/{}'.format(exchange_symbol), None)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
# TODO: filter by type
|
||||||
|
result = dict()
|
||||||
|
for order_type in data:
|
||||||
|
result[order_type] = []
|
||||||
|
|
||||||
|
for entry in data[order_type]:
|
||||||
|
result[order_type].append(dict(
|
||||||
|
rate=float(entry['price']),
|
||||||
|
quantity=float(entry['amount'])
|
||||||
|
))
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"neobtc": {
|
||||||
|
"symbol": "neo_btc",
|
||||||
|
"start_date": "2017-09-07",
|
||||||
|
"precision": 5
|
||||||
|
},
|
||||||
|
"neousd": {
|
||||||
|
"symbol": "neo_usd",
|
||||||
|
"start_date": "2017-09-07"
|
||||||
|
},
|
||||||
|
"neoeth": {
|
||||||
|
"symbol": "neo_eth",
|
||||||
|
"start_date": "2017-09-07"
|
||||||
|
},
|
||||||
|
"btcusd": {
|
||||||
|
"symbol": "btc_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"bchusd": {
|
||||||
|
"symbol": "bch_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"ltcusd": {
|
||||||
|
"symbol": "ltc_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"ltcbtc": {
|
||||||
|
"symbol": "ltc_btc",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"ethusd": {
|
||||||
|
"symbol": "eth_usd",
|
||||||
|
"start_date": "2017-01-01"
|
||||||
|
},
|
||||||
|
"ethbtc": {
|
||||||
|
"symbol": "eth_btc",
|
||||||
|
"start_date": "2017-01-01"
|
||||||
|
},
|
||||||
|
"etcbtc": {
|
||||||
|
"symbol": "etc_btc",
|
||||||
|
"start_date": "2017-01-01"
|
||||||
|
},
|
||||||
|
"etcusd": {
|
||||||
|
"symbol": "etc_usd",
|
||||||
|
"start_date": "2017-01-01"
|
||||||
|
},
|
||||||
|
"rrtusd": {
|
||||||
|
"symbol": "rrt_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"rrtbtc": {
|
||||||
|
"symbol": "rrt_btc",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"zecusd": {
|
||||||
|
"symbol": "zec_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"zecbtc": {
|
||||||
|
"symbol": "zec_btc",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"xmrusd": {
|
||||||
|
"symbol": "xmr_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"xmrbtc": {
|
||||||
|
"symbol": "xmr_btc",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"dshusd": {
|
||||||
|
"symbol": "dsh_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"dshbtc": {
|
||||||
|
"symbol": "dsh_btc",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"bccbtc": {
|
||||||
|
"symbol": "bcc_btc",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"bcubtc": {
|
||||||
|
"symbol": "bcu_btc",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"bccusd": {
|
||||||
|
"symbol": "bcc_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"bcuusd": {
|
||||||
|
"symbol": "bcu_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"xrpusd": {
|
||||||
|
"symbol": "xrp_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"xrpbtc": {
|
||||||
|
"symbol": "xrp_btc",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"iotusd": {
|
||||||
|
"symbol": "iot_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"iotbtc": {
|
||||||
|
"symbol": "iot_btc",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"ioteth": {
|
||||||
|
"symbol": "iot_eth",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"eosusd": {
|
||||||
|
"symbol": "eos_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"eosbtc": {
|
||||||
|
"symbol": "eos_btc",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
|
"eoseth": {
|
||||||
|
"symbol": "eos_eth",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from logbook import Logger
|
||||||
|
from six.moves import urllib
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
from catalyst.exchange.bittrex.bittrex_api import Bittrex_api
|
||||||
|
from catalyst.exchange.exchange import Exchange
|
||||||
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
|
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
||||||
|
ExchangeRequestError, InvalidOrderStyle, OrderNotFound, OrderCancelError, \
|
||||||
|
CreateOrderError
|
||||||
|
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename, \
|
||||||
|
download_exchange_symbols, get_symbols_string
|
||||||
|
from catalyst.finance.execution import LimitOrder, StopLimitOrder
|
||||||
|
from catalyst.finance.order import Order, ORDER_STATUS
|
||||||
|
|
||||||
|
# TODO: consider using this: https://github.com/mondeja/bittrex_v2
|
||||||
|
|
||||||
|
log = Logger('Bittrex', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
URL2 = 'https://bittrex.com/Api/v2.0'
|
||||||
|
|
||||||
|
|
||||||
|
class Bittrex(Exchange):
|
||||||
|
def __init__(self, key, secret, base_currency, portfolio=None):
|
||||||
|
self.api = Bittrex_api(key=key, secret=secret)
|
||||||
|
self.name = 'bittrex'
|
||||||
|
self.color = 'blue'
|
||||||
|
self.base_currency = base_currency
|
||||||
|
self._portfolio = portfolio
|
||||||
|
|
||||||
|
self.num_candles_limit = 2000
|
||||||
|
|
||||||
|
# Not sure what the rate limit is but trying to play it safe
|
||||||
|
# https://bitcoin.stackexchange.com/questions/53778/bittrex-api-rate-limit
|
||||||
|
self.max_requests_per_minute = 60
|
||||||
|
self.request_cpt = dict()
|
||||||
|
|
||||||
|
self.minute_writer = None
|
||||||
|
self.minute_reader = None
|
||||||
|
|
||||||
|
self.assets = dict()
|
||||||
|
self.load_assets()
|
||||||
|
|
||||||
|
self.local_assets = dict()
|
||||||
|
self.load_assets(is_local=True)
|
||||||
|
|
||||||
|
self.bundle = ExchangeBundle(self.name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_skew(self):
|
||||||
|
# TODO: research the time skew conditions
|
||||||
|
return pd.Timedelta('0s')
|
||||||
|
|
||||||
|
def sanitize_curency_symbol(self, exchange_symbol):
|
||||||
|
"""
|
||||||
|
Helper method used to build the universal pair.
|
||||||
|
Include any symbol mapping here if appropriate.
|
||||||
|
|
||||||
|
:param exchange_symbol:
|
||||||
|
:return universal_symbol:
|
||||||
|
"""
|
||||||
|
return exchange_symbol.lower()
|
||||||
|
|
||||||
|
def get_balances(self):
|
||||||
|
balances = self.api.getbalances()
|
||||||
|
try:
|
||||||
|
log.debug('retrieving wallet balances')
|
||||||
|
self.ask_request()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
std_balances = dict()
|
||||||
|
try:
|
||||||
|
for balance in balances:
|
||||||
|
currency = balance['Currency'].lower()
|
||||||
|
std_balances[currency] = balance['Available']
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
raise ExchangeRequestError(error=balances)
|
||||||
|
|
||||||
|
return std_balances
|
||||||
|
|
||||||
|
def create_order(self, asset, amount, is_buy, style):
|
||||||
|
log.info('creating {} order'.format('buy' if is_buy else 'sell'))
|
||||||
|
exchange_symbol = self.get_symbol(asset)
|
||||||
|
|
||||||
|
if isinstance(style, LimitOrder) or isinstance(style, StopLimitOrder):
|
||||||
|
if isinstance(style, StopLimitOrder):
|
||||||
|
log.warn('{} will ignore the stop price'.format(self.name))
|
||||||
|
|
||||||
|
price = style.get_limit_price(is_buy)
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
if is_buy:
|
||||||
|
order_status = self.api.buylimit(exchange_symbol, amount,
|
||||||
|
price)
|
||||||
|
else:
|
||||||
|
order_status = self.api.selllimit(exchange_symbol,
|
||||||
|
abs(amount), price)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'uuid' in order_status:
|
||||||
|
order_id = order_status['uuid']
|
||||||
|
order = Order(
|
||||||
|
dt=pd.Timestamp.utcnow(),
|
||||||
|
asset=asset,
|
||||||
|
amount=amount,
|
||||||
|
stop=style.get_stop_price(is_buy),
|
||||||
|
limit=style.get_limit_price(is_buy),
|
||||||
|
id=order_id
|
||||||
|
)
|
||||||
|
return order
|
||||||
|
else:
|
||||||
|
if order_status == 'INSUFFICIENT_FUNDS':
|
||||||
|
log.warn('not enough funds to create order')
|
||||||
|
return None
|
||||||
|
elif order_status == 'DUST_TRADE_DISALLOWED_MIN_VALUE_50K_SAT':
|
||||||
|
log.warn('Your order is too small, order at least 50K'
|
||||||
|
' Satoshi')
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise CreateOrderError(
|
||||||
|
exchange=self.name,
|
||||||
|
error=order_status
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise InvalidOrderStyle(exchange=self.name,
|
||||||
|
style=style.__class__.__name__)
|
||||||
|
|
||||||
|
def get_open_orders(self, asset):
|
||||||
|
symbol = self.get_symbol(asset)
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
open_orders = self.api.getopenorders(symbol)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
orders = list()
|
||||||
|
for order_status in open_orders:
|
||||||
|
order = self._create_order(order_status)
|
||||||
|
orders.append(order)
|
||||||
|
|
||||||
|
return orders
|
||||||
|
|
||||||
|
def _create_order(self, order_status):
|
||||||
|
log.info(
|
||||||
|
'creating catalyst order from Bittrex {}'.format(order_status))
|
||||||
|
if order_status['CancelInitiated']:
|
||||||
|
status = ORDER_STATUS.CANCELLED
|
||||||
|
elif order_status['Closed'] is not None:
|
||||||
|
status = ORDER_STATUS.FILLED
|
||||||
|
else:
|
||||||
|
status = ORDER_STATUS.OPEN
|
||||||
|
|
||||||
|
date = pd.to_datetime(order_status['Opened'], utc=True)
|
||||||
|
amount = order_status['Quantity']
|
||||||
|
filled = amount - order_status['QuantityRemaining']
|
||||||
|
order = Order(
|
||||||
|
dt=date,
|
||||||
|
asset=self.assets[order_status['Exchange']],
|
||||||
|
amount=amount,
|
||||||
|
stop=None, # Not yet supported by Bittrex
|
||||||
|
limit=order_status['Limit'],
|
||||||
|
filled=filled,
|
||||||
|
id=order_status['OrderUuid'],
|
||||||
|
commission=order_status['CommissionPaid']
|
||||||
|
)
|
||||||
|
order.status = status
|
||||||
|
|
||||||
|
executed_price = order_status['PricePerUnit']
|
||||||
|
|
||||||
|
return order, executed_price
|
||||||
|
|
||||||
|
def get_order(self, order_id):
|
||||||
|
log.info('retrieving order {}'.format(order_id))
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
order_status = self.api.getorder(order_id)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if order_status is None:
|
||||||
|
raise OrderNotFound(order_id=order_id, exchange=self.name)
|
||||||
|
|
||||||
|
return self._create_order(order_status)
|
||||||
|
|
||||||
|
def cancel_order(self, order_param):
|
||||||
|
order_id = order_param.id \
|
||||||
|
if isinstance(order_param, Order) else order_param
|
||||||
|
log.info('cancelling order {}'.format(order_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
status = self.api.cancel(order_id)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'message' in status:
|
||||||
|
raise OrderCancelError(
|
||||||
|
order_id=order_id,
|
||||||
|
exchange=self.name,
|
||||||
|
error=status['message']
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_candles(self, freq, assets, bar_count=None,
|
||||||
|
start_dt=None, end_dt=None):
|
||||||
|
"""
|
||||||
|
Supported Intervals
|
||||||
|
-------------------
|
||||||
|
day, oneMin, fiveMin, thirtyMin, hour
|
||||||
|
|
||||||
|
:param freq:
|
||||||
|
:param assets:
|
||||||
|
:param bar_count:
|
||||||
|
:param start_dt
|
||||||
|
:param end_dt
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: this has no effect at the moment
|
||||||
|
if end_dt is None:
|
||||||
|
end_dt = pd.Timestamp.utcnow()
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
'retrieving {bars} {freq} candles on {exchange} from '
|
||||||
|
'{end_dt} for markets {symbols}, '.format(
|
||||||
|
bars=bar_count,
|
||||||
|
freq=freq,
|
||||||
|
exchange=self.name,
|
||||||
|
end_dt=end_dt,
|
||||||
|
symbols=get_symbols_string(assets)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if freq == '1T':
|
||||||
|
frequency = 'oneMin'
|
||||||
|
elif freq == '5T':
|
||||||
|
frequency = 'fiveMin'
|
||||||
|
elif freq == '30T':
|
||||||
|
frequency = 'thirtyMin'
|
||||||
|
elif freq == '60T':
|
||||||
|
frequency = 'hour'
|
||||||
|
elif freq == '1D':
|
||||||
|
frequency = 'day'
|
||||||
|
else:
|
||||||
|
raise InvalidHistoryFrequencyError(frequency=freq)
|
||||||
|
|
||||||
|
# Making sure that assets are iterable
|
||||||
|
asset_list = [assets] if isinstance(assets, TradingPair) else assets
|
||||||
|
for asset in asset_list:
|
||||||
|
end = int(time.mktime(end_dt.timetuple()))
|
||||||
|
url = '{url}/pub/market/GetTicks?marketName={symbol}' \
|
||||||
|
'&tickInterval={frequency}&_={end}'.format(
|
||||||
|
url=URL2,
|
||||||
|
symbol=self.get_symbol(asset),
|
||||||
|
frequency=frequency,
|
||||||
|
end=end
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(urllib.request.urlopen(url).read().decode())
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if data['message']:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='Unable to fetch candles {}'.format(data['message'])
|
||||||
|
)
|
||||||
|
|
||||||
|
candles = data['result']
|
||||||
|
|
||||||
|
def ohlc_from_candle(candle):
|
||||||
|
ohlc = dict(
|
||||||
|
open=candle['O'],
|
||||||
|
high=candle['H'],
|
||||||
|
low=candle['L'],
|
||||||
|
close=candle['C'],
|
||||||
|
volume=candle['V'],
|
||||||
|
price=candle['C'],
|
||||||
|
last_traded=pd.to_datetime(candle['T'], utc=True)
|
||||||
|
)
|
||||||
|
return ohlc
|
||||||
|
|
||||||
|
ordered_candles = list(reversed(candles))
|
||||||
|
ohlc_map = dict()
|
||||||
|
if bar_count is None:
|
||||||
|
ohlc_map[asset] = ohlc_from_candle(ordered_candles[0])
|
||||||
|
else:
|
||||||
|
# TODO: optimize
|
||||||
|
ohlc_bars = []
|
||||||
|
for candle in ordered_candles[:bar_count]:
|
||||||
|
ohlc = ohlc_from_candle(candle)
|
||||||
|
ohlc_bars.append(ohlc)
|
||||||
|
|
||||||
|
ohlc_map[asset] = ohlc_bars
|
||||||
|
|
||||||
|
return ohlc_map[assets] \
|
||||||
|
if isinstance(assets, TradingPair) else ohlc_map
|
||||||
|
|
||||||
|
def tickers(self, assets):
|
||||||
|
"""
|
||||||
|
As of v1.1, Bittrex only allows one ticker at the time.
|
||||||
|
So we have to make multiple calls to fetch multiple assets.
|
||||||
|
|
||||||
|
:param assets:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
log.info('retrieving tickers')
|
||||||
|
|
||||||
|
ticks = dict()
|
||||||
|
for asset in assets:
|
||||||
|
symbol = self.get_symbol(asset)
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
ticker = self.api.getticker(symbol)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
# TODO: catch invalid ticker
|
||||||
|
ticks[asset] = dict(
|
||||||
|
timestamp=pd.Timestamp.utcnow(),
|
||||||
|
bid=ticker['Bid'],
|
||||||
|
ask=ticker['Ask'],
|
||||||
|
last_price=ticker['Last']
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug('got tickers {}'.format(ticks))
|
||||||
|
return ticks
|
||||||
|
|
||||||
|
def get_account(self):
|
||||||
|
log.info('retrieving account data')
|
||||||
|
pass
|
||||||
|
|
||||||
|
def generate_symbols_json(self, filename=None):
|
||||||
|
symbol_map = {}
|
||||||
|
|
||||||
|
fn, r = download_exchange_symbols(self.name)
|
||||||
|
with open(fn) as data_file:
|
||||||
|
cached_symbols = json.load(data_file)
|
||||||
|
|
||||||
|
markets = self.api.getmarkets()
|
||||||
|
for market in markets:
|
||||||
|
exchange_symbol = market['MarketName']
|
||||||
|
symbol = '{market}_{base}'.format(
|
||||||
|
market=self.sanitize_curency_symbol(market['MarketCurrency']),
|
||||||
|
base=self.sanitize_curency_symbol(market['BaseCurrency'])
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_daily = cached_symbols[exchange_symbol]['end_daily']
|
||||||
|
except KeyError as e:
|
||||||
|
end_daily = 'N/A'
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_minute = cached_symbols[exchange_symbol]['end_minute']
|
||||||
|
except KeyError as e:
|
||||||
|
end_minute = 'N/A'
|
||||||
|
|
||||||
|
symbol_map[exchange_symbol] = dict(
|
||||||
|
symbol=symbol,
|
||||||
|
start_date=pd.to_datetime(market['Created'],
|
||||||
|
utc=True).strftime("%Y-%m-%d"),
|
||||||
|
end_daily=end_daily,
|
||||||
|
end_minute=end_minute,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (filename is None):
|
||||||
|
filename = get_exchange_symbols_filename(self.name)
|
||||||
|
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(symbol_map, f, sort_keys=True, indent=2,
|
||||||
|
separators=(',', ':'))
|
||||||
|
|
||||||
|
def get_orderbook(self, asset, order_type='all', limit=100):
|
||||||
|
if order_type == 'all':
|
||||||
|
order_type = 'both'
|
||||||
|
elif order_type == 'bid':
|
||||||
|
order_type = 'buy'
|
||||||
|
elif order_type == 'ask':
|
||||||
|
order_type = 'sell'
|
||||||
|
else:
|
||||||
|
raise ValueError('invalid type')
|
||||||
|
|
||||||
|
exchange_symbol = asset.exchange_symbol
|
||||||
|
data = self.api.getorderbook(
|
||||||
|
market=exchange_symbol,
|
||||||
|
type=order_type,
|
||||||
|
depth=100
|
||||||
|
)
|
||||||
|
|
||||||
|
result = dict()
|
||||||
|
for exchange_type in data:
|
||||||
|
if exchange_type == 'buy':
|
||||||
|
order_type = 'bids'
|
||||||
|
elif exchange_type == 'sell':
|
||||||
|
order_type = 'asks'
|
||||||
|
|
||||||
|
result[order_type] = []
|
||||||
|
for entry in data[exchange_type]:
|
||||||
|
result[order_type].append(dict(
|
||||||
|
rate=entry['Rate'],
|
||||||
|
quantity=entry['Quantity']
|
||||||
|
))
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
# Workaround for backwards compatibility
|
||||||
|
# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7
|
||||||
|
from six.moves import urllib
|
||||||
|
|
||||||
|
urlopen = urllib.request.urlopen
|
||||||
|
|
||||||
|
|
||||||
|
class Bittrex_api(object):
|
||||||
|
def __init__(self, key, secret):
|
||||||
|
self.key = key
|
||||||
|
self.secret = secret
|
||||||
|
self.public = ['getmarkets', 'getcurrencies', 'getticker',
|
||||||
|
'getmarketsummaries', 'getmarketsummary',
|
||||||
|
'getorderbook', 'getmarkethistory']
|
||||||
|
self.market = ['buylimit', 'buymarket', 'selllimit', 'sellmarket',
|
||||||
|
'cancel', 'getopenorders']
|
||||||
|
self.account = ['getbalances', 'getbalance', 'getdepositaddress',
|
||||||
|
'withdraw', 'getorder', 'getorderhistory',
|
||||||
|
'getwithdrawalhistory', 'getdeposithistory']
|
||||||
|
|
||||||
|
def query(self, method, values={}):
|
||||||
|
if method in self.public:
|
||||||
|
url = 'https://bittrex.com/api/v1.1/public/'
|
||||||
|
elif method in self.market:
|
||||||
|
url = 'https://bittrex.com/api/v1.1/market/'
|
||||||
|
elif method in self.account:
|
||||||
|
url = 'https://bittrex.com/api/v1.1/account/'
|
||||||
|
else:
|
||||||
|
return 'Something went wrong, sorry.'
|
||||||
|
|
||||||
|
url += method + '?' + urllib.parse.urlencode(values)
|
||||||
|
|
||||||
|
if method not in self.public:
|
||||||
|
url += '&apikey=' + self.key
|
||||||
|
url += '&nonce=' + str(int(time.time()))
|
||||||
|
|
||||||
|
signature = hmac.new(self.secret.encode('utf-8'),
|
||||||
|
url.encode('utf-8'),
|
||||||
|
hashlib.sha512).hexdigest()
|
||||||
|
headers = {'apisign': signature}
|
||||||
|
else:
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
response = json.loads(urlopen(
|
||||||
|
req, context=ssl._create_unverified_context()).read())
|
||||||
|
|
||||||
|
if response["result"]:
|
||||||
|
return response["result"]
|
||||||
|
else:
|
||||||
|
return response["message"]
|
||||||
|
|
||||||
|
def getmarkets(self):
|
||||||
|
return self.query('getmarkets')
|
||||||
|
|
||||||
|
def getcurrencies(self):
|
||||||
|
return self.query('getcurrencies')
|
||||||
|
|
||||||
|
def getticker(self, market):
|
||||||
|
return self.query('getticker', {'market': market})
|
||||||
|
|
||||||
|
def getmarketsummaries(self):
|
||||||
|
return self.query('getmarketsummaries')
|
||||||
|
|
||||||
|
def getmarketsummary(self, market):
|
||||||
|
return self.query('getmarketsummary', {'market': market})
|
||||||
|
|
||||||
|
def getorderbook(self, market, type, depth=20):
|
||||||
|
return self.query('getorderbook',
|
||||||
|
{'market': market, 'type': type, 'depth': depth})
|
||||||
|
|
||||||
|
def getmarkethistory(self, market, count=20):
|
||||||
|
return self.query('getmarkethistory',
|
||||||
|
{'market': market, 'count': count})
|
||||||
|
|
||||||
|
def buylimit(self, market, quantity, rate):
|
||||||
|
return self.query('buylimit', {'market': market, 'quantity': quantity,
|
||||||
|
'rate': rate})
|
||||||
|
|
||||||
|
def buymarket(self, market, quantity):
|
||||||
|
return self.query('buymarket',
|
||||||
|
{'market': market, 'quantity': quantity})
|
||||||
|
|
||||||
|
def selllimit(self, market, quantity, rate):
|
||||||
|
return self.query('selllimit', {'market': market, 'quantity': quantity,
|
||||||
|
'rate': rate})
|
||||||
|
|
||||||
|
def sellmarket(self, market, quantity):
|
||||||
|
return self.query('sellmarket',
|
||||||
|
{'market': market, 'quantity': quantity})
|
||||||
|
|
||||||
|
def cancel(self, uuid):
|
||||||
|
return self.query('cancel', {'uuid': uuid})
|
||||||
|
|
||||||
|
def getopenorders(self, market):
|
||||||
|
return self.query('getopenorders', {'market': market})
|
||||||
|
|
||||||
|
def getbalances(self):
|
||||||
|
return self.query('getbalances')
|
||||||
|
|
||||||
|
def getbalance(self, currency):
|
||||||
|
return self.query('getbalance', {'currency': currency})
|
||||||
|
|
||||||
|
def getdepositaddress(self, currency):
|
||||||
|
return self.query('getdepositaddress', {'currency': currency})
|
||||||
|
|
||||||
|
def withdraw(self, currency, quantity, address):
|
||||||
|
return self.query('withdraw',
|
||||||
|
{'currency': currency, 'quantity': quantity,
|
||||||
|
'address': address})
|
||||||
|
|
||||||
|
def getorder(self, uuid):
|
||||||
|
return self.query('getorder', {'uuid': uuid})
|
||||||
|
|
||||||
|
def getorderhistory(self, market, count):
|
||||||
|
return self.query('getorderhistory',
|
||||||
|
{'market': market, 'count': count})
|
||||||
|
|
||||||
|
def getwithdrawalhistory(self, currency, count):
|
||||||
|
return self.query('getwithdrawalhistory',
|
||||||
|
{'currency': currency, 'count': count})
|
||||||
|
|
||||||
|
def getdeposithistory(self, currency, count):
|
||||||
|
return self.query('getdeposithistory',
|
||||||
|
{'currency': currency, 'count': count})
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from catalyst.data.bundles import register
|
||||||
|
from catalyst.exchange.exchange_bundle import exchange_bundle
|
||||||
|
|
||||||
|
symbols = (
|
||||||
|
'neo_btc',
|
||||||
|
)
|
||||||
|
register('exchange_bitfinex', exchange_bundle('bitfinex', symbols))
|
||||||
@@ -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.utils.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'
|
||||||
@@ -78,8 +80,9 @@ def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
|||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
url = 'https://s3.amazonaws.com/enigmaco/catalyst-bundles/' \
|
url = 'https://s3.amazonaws.com/enigmaco/catalyst-bundles/' \
|
||||||
'exchange-{exchange}/{name}.tar.gz'.format(
|
'exchange-{exchange}/{name}.tar.gz'.format(
|
||||||
exchange=exchange_name,
|
exchange=exchange_name,
|
||||||
name=name)
|
name=name
|
||||||
|
)
|
||||||
|
|
||||||
bytes = download_without_progress(url)
|
bytes = download_without_progress(url)
|
||||||
with tarfile.open('r', fileobj=bytes) as tar:
|
with tarfile.open('r', fileobj=bytes) as tar:
|
||||||
@@ -190,10 +193,8 @@ def get_period_label(dt, data_frequency):
|
|||||||
str
|
str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if data_frequency == 'minute':
|
return '{}-{:02d}'.format(dt.year, dt.month) if data_frequency == 'minute' \
|
||||||
return '{}-{:02d}'.format(dt.year, dt.month)
|
else '{}'.format(dt.year)
|
||||||
else:
|
|
||||||
return '{}'.format(dt.year)
|
|
||||||
|
|
||||||
|
|
||||||
def get_month_start_end(dt, first_day=None, last_day=None):
|
def get_month_start_end(dt, first_day=None, last_day=None):
|
||||||
@@ -314,7 +315,7 @@ def range_in_bundle(asset, start_dt, end_dt, reader):
|
|||||||
if np.isnan(close):
|
if np.isnan(close):
|
||||||
has_data = False
|
has_data = False
|
||||||
|
|
||||||
except Exception:
|
except Exception as e:
|
||||||
has_data = False
|
has_data = False
|
||||||
|
|
||||||
return has_data
|
return has_data
|
||||||
@@ -1,28 +1,25 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import six
|
from ccxt import ExchangeNotAvailable
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
from ccxt import ExchangeNotAvailable, InvalidOrder
|
|
||||||
from logbook import Logger
|
|
||||||
from six import string_types
|
from six import string_types
|
||||||
|
|
||||||
|
from catalyst.finance.order import Order, ORDER_STATUS
|
||||||
|
|
||||||
from catalyst.algorithm import MarketOrder
|
from catalyst.algorithm import MarketOrder
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.exchange.exchange import Exchange
|
from catalyst.exchange.exchange import Exchange, ExchangeLimitOrder
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
||||||
ExchangeSymbolsNotFound, ExchangeRequestError, InvalidOrderStyle, \
|
ExchangeSymbolsNotFound, ExchangeRequestError, InvalidOrderStyle, \
|
||||||
ExchangeNotFoundError, CreateOrderError, InvalidHistoryTimeframeError
|
ExchangeNotFoundError
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
from catalyst.exchange.exchange_utils import mixin_market_params, \
|
||||||
from catalyst.exchange.utils.exchange_utils import mixin_market_params, \
|
from_ms_timestamp
|
||||||
from_ms_timestamp, get_epoch, get_exchange_folder, get_catalyst_symbol, \
|
|
||||||
get_exchange_auth
|
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
|
||||||
|
|
||||||
log = Logger('CCXT', level=LOG_LEVEL)
|
log = Logger('CCXT', level=LOG_LEVEL)
|
||||||
|
|
||||||
@@ -37,7 +34,8 @@ SUPPORTED_EXCHANGES = dict(
|
|||||||
|
|
||||||
|
|
||||||
class CCXT(Exchange):
|
class CCXT(Exchange):
|
||||||
def __init__(self, exchange_name, key, secret, base_currency):
|
def __init__(self, exchange_name, key, secret, base_currency,
|
||||||
|
portfolio=None):
|
||||||
log.debug(
|
log.debug(
|
||||||
'finding {} in CCXT exchanges:\n{}'.format(
|
'finding {} in CCXT exchanges:\n{}'.format(
|
||||||
exchange_name, ccxt.exchanges
|
exchange_name, ccxt.exchanges
|
||||||
@@ -61,103 +59,23 @@ class CCXT(Exchange):
|
|||||||
|
|
||||||
self._symbol_maps = [None, None]
|
self._symbol_maps = [None, None]
|
||||||
|
|
||||||
|
markets_symbols = self.api.load_markets()
|
||||||
|
log.debug('the markets:\n{}'.format(markets_symbols))
|
||||||
|
|
||||||
self.name = exchange_name
|
self.name = exchange_name
|
||||||
|
|
||||||
|
self.markets = self.api.fetch_markets()
|
||||||
|
self.load_assets()
|
||||||
|
|
||||||
self.base_currency = base_currency
|
self.base_currency = base_currency
|
||||||
|
self._portfolio = portfolio
|
||||||
self.transactions = defaultdict(list)
|
self.transactions = defaultdict(list)
|
||||||
|
|
||||||
self.num_candles_limit = 2000
|
self.num_candles_limit = 2000
|
||||||
self.max_requests_per_minute = 60
|
self.max_requests_per_minute = 60
|
||||||
self.low_balance_threshold = 0.1
|
|
||||||
self.request_cpt = dict()
|
self.request_cpt = dict()
|
||||||
|
|
||||||
self.bundle = ExchangeBundle(self.name)
|
self.bundle = ExchangeBundle(self.name)
|
||||||
self.markets = None
|
|
||||||
self._is_init = False
|
|
||||||
|
|
||||||
def init(self):
|
|
||||||
if self._is_init:
|
|
||||||
return
|
|
||||||
|
|
||||||
exchange_folder = get_exchange_folder(self.name)
|
|
||||||
filename = os.path.join(exchange_folder, 'cctx_markets.json')
|
|
||||||
|
|
||||||
if os.path.exists(filename):
|
|
||||||
timestamp = os.path.getmtime(filename)
|
|
||||||
dt = pd.to_datetime(timestamp, unit='s', utc=True)
|
|
||||||
|
|
||||||
if dt >= pd.Timestamp.utcnow().floor('1D'):
|
|
||||||
with open(filename) as f:
|
|
||||||
self.markets = json.load(f)
|
|
||||||
|
|
||||||
log.debug('loaded markets for {}'.format(self.name))
|
|
||||||
|
|
||||||
if self.markets is None:
|
|
||||||
try:
|
|
||||||
markets_symbols = self.api.load_markets()
|
|
||||||
log.debug(
|
|
||||||
'fetching {} markets:\n{}'.format(
|
|
||||||
self.name, markets_symbols
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.markets = self.api.fetch_markets()
|
|
||||||
with open(filename, 'w+') as f:
|
|
||||||
json.dump(self.markets, f, indent=4)
|
|
||||||
|
|
||||||
except ExchangeNotAvailable as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
self.load_assets()
|
|
||||||
self._is_init = True
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def find_exchanges(features=None, is_authenticated=False):
|
|
||||||
ccxt_features = []
|
|
||||||
if features is not None:
|
|
||||||
for feature in features:
|
|
||||||
if not feature.endswith('Bundle'):
|
|
||||||
ccxt_features.append(feature)
|
|
||||||
|
|
||||||
exchange_names = []
|
|
||||||
for exchange_name in ccxt.exchanges:
|
|
||||||
if is_authenticated:
|
|
||||||
exchange_auth = get_exchange_auth(exchange_name)
|
|
||||||
|
|
||||||
has_auth = (exchange_auth['key'] != ''
|
|
||||||
and exchange_auth['secret'] != '')
|
|
||||||
|
|
||||||
if not has_auth:
|
|
||||||
continue
|
|
||||||
|
|
||||||
log.debug('loading exchange: {}'.format(exchange_name))
|
|
||||||
exchange = getattr(ccxt, exchange_name)()
|
|
||||||
|
|
||||||
if ccxt_features is None:
|
|
||||||
has_feature = True
|
|
||||||
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
has_feature = all(
|
|
||||||
[exchange.has[feature] for feature in ccxt_features]
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
has_feature = False
|
|
||||||
|
|
||||||
if has_feature:
|
|
||||||
try:
|
|
||||||
log.info('initializing {}'.format(exchange_name))
|
|
||||||
exchange_names.append(exchange_name)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log.warn(
|
|
||||||
'unable to initialize exchange {}: {}'.format(
|
|
||||||
exchange_name, e
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return exchange_names
|
|
||||||
|
|
||||||
def account(self):
|
def account(self):
|
||||||
return None
|
return None
|
||||||
@@ -165,30 +83,6 @@ class CCXT(Exchange):
|
|||||||
def time_skew(self):
|
def time_skew(self):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_candle_frequencies(self, data_frequency=None):
|
|
||||||
frequencies = []
|
|
||||||
try:
|
|
||||||
for timeframe in self.api.timeframes:
|
|
||||||
freq = CCXT.get_frequency(timeframe, raise_error=False)
|
|
||||||
|
|
||||||
# TODO: support all frequencies
|
|
||||||
if data_frequency == 'minute' and not freq.endswith('T'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif data_frequency == 'daily' and not freq.endswith('D'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
frequencies.append(freq)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log.warn(
|
|
||||||
'candle frequencies not available for exchange {}'.format(
|
|
||||||
self.name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return frequencies
|
|
||||||
|
|
||||||
def get_market(self, symbol):
|
def get_market(self, symbol):
|
||||||
"""
|
"""
|
||||||
The CCXT market.
|
The CCXT market.
|
||||||
@@ -210,7 +104,7 @@ class CCXT(Exchange):
|
|||||||
)
|
)
|
||||||
return market
|
return market
|
||||||
|
|
||||||
def get_symbol(self, asset_or_symbol, source='catalyst'):
|
def get_symbol(self, asset_or_symbol):
|
||||||
"""
|
"""
|
||||||
The CCXT symbol.
|
The CCXT symbol.
|
||||||
|
|
||||||
@@ -222,109 +116,36 @@ class CCXT(Exchange):
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
symbol = asset_or_symbol if isinstance(
|
||||||
|
asset_or_symbol, string_types
|
||||||
|
) else asset_or_symbol.symbol
|
||||||
|
|
||||||
if source == 'ccxt':
|
parts = symbol.split('_')
|
||||||
if isinstance(asset_or_symbol, string_types):
|
return '{}/{}'.format(parts[0].upper(), parts[1].upper())
|
||||||
parts = asset_or_symbol.split('/')
|
|
||||||
return '{}_{}'.format(parts[0].lower(), parts[1].lower())
|
|
||||||
|
|
||||||
else:
|
def get_catalyst_symbol(self, market_or_symbol):
|
||||||
return asset_or_symbol.symbol
|
|
||||||
|
|
||||||
else:
|
|
||||||
symbol = asset_or_symbol if isinstance(
|
|
||||||
asset_or_symbol, string_types
|
|
||||||
) else asset_or_symbol.symbol
|
|
||||||
|
|
||||||
parts = symbol.split('_')
|
|
||||||
return '{}/{}'.format(parts[0].upper(), parts[1].upper())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def map_frequency(value, source='ccxt', raise_error=True):
|
|
||||||
"""
|
"""
|
||||||
Map a frequency value between CCXT and Catalyst
|
The Catalyst symbol.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
value: str
|
market_or_symbol
|
||||||
source: str
|
|
||||||
raise_error: bool
|
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
The Pandas offset aliases supported by Catalyst:
|
|
||||||
Alias Description
|
|
||||||
W weekly frequency
|
|
||||||
M month end frequency
|
|
||||||
D calendar day frequency
|
|
||||||
H hourly frequency
|
|
||||||
T, min minutely frequency
|
|
||||||
|
|
||||||
The CCXT timeframes:
|
|
||||||
'1m': '1minute',
|
|
||||||
'1h': '1hour',
|
|
||||||
'1d': '1day',
|
|
||||||
'1w': '1week',
|
|
||||||
'1M': '1month',
|
|
||||||
'1y': '1year',
|
|
||||||
"""
|
"""
|
||||||
match = re.match(
|
if isinstance(market_or_symbol, string_types):
|
||||||
r'([0-9].*)?(m|M|d|D|h|H|T|w|W|min)', value, re.M | re.I
|
parts = market_or_symbol.split('/')
|
||||||
)
|
return '{}_{}'.format(parts[0].lower(), parts[1].lower())
|
||||||
if match:
|
|
||||||
candle_size = int(match.group(1)) \
|
|
||||||
if match.group(1) else 1
|
|
||||||
|
|
||||||
unit = match.group(2)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unable to parse frequency or timeframe')
|
return '{}_{}'.format(
|
||||||
|
market_or_symbol['base'].lower(),
|
||||||
|
market_or_symbol['quote'].lower(),
|
||||||
|
)
|
||||||
|
|
||||||
if source == 'ccxt':
|
def get_timeframe(self, freq):
|
||||||
if unit == 'd':
|
|
||||||
result = '{}D'.format(candle_size)
|
|
||||||
|
|
||||||
elif unit == 'm':
|
|
||||||
result = '{}T'.format(candle_size)
|
|
||||||
|
|
||||||
elif unit == 'h':
|
|
||||||
result = '{}H'.format(candle_size)
|
|
||||||
|
|
||||||
elif unit == 'w':
|
|
||||||
result = '{}W'.format(candle_size)
|
|
||||||
|
|
||||||
elif unit == 'M':
|
|
||||||
result = '{}M'.format(candle_size)
|
|
||||||
|
|
||||||
elif raise_error:
|
|
||||||
raise InvalidHistoryTimeframeError(timeframe=value)
|
|
||||||
|
|
||||||
else:
|
|
||||||
if unit == 'D':
|
|
||||||
result = '{}d'.format(candle_size)
|
|
||||||
|
|
||||||
elif unit == 'min' or unit == 'T':
|
|
||||||
result = '{}m'.format(candle_size)
|
|
||||||
|
|
||||||
elif unit == 'H':
|
|
||||||
result = '{}h'.format(candle_size)
|
|
||||||
|
|
||||||
elif unit == 'W':
|
|
||||||
result = '{}w'.format(candle_size)
|
|
||||||
|
|
||||||
elif unit == 'M':
|
|
||||||
result = '{}M'.format(candle_size)
|
|
||||||
|
|
||||||
elif raise_error:
|
|
||||||
raise InvalidHistoryFrequencyError(frequency=value)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_timeframe(freq, raise_error=True):
|
|
||||||
"""
|
"""
|
||||||
The CCXT timeframe from the Catalyst frequency.
|
The CCXT timeframe from the Catalyst frequency.
|
||||||
|
|
||||||
@@ -338,43 +159,33 @@ class CCXT(Exchange):
|
|||||||
str
|
str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return CCXT.map_frequency(
|
freq_match = re.match(r'([0-9].*)?(m|M|d|D|h|H|T)', freq, re.M | re.I)
|
||||||
freq, source='catalyst', raise_error=raise_error
|
if freq_match:
|
||||||
)
|
candle_size = int(freq_match.group(1)) \
|
||||||
|
if freq_match.group(1) else 1
|
||||||
|
|
||||||
@staticmethod
|
unit = freq_match.group(2)
|
||||||
def get_frequency(timeframe, raise_error=True):
|
|
||||||
"""
|
|
||||||
Test Catalyst frequency from the CCXT timeframe
|
|
||||||
|
|
||||||
Catalyst uses the Pandas offset alias convention:
|
else:
|
||||||
http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
|
raise InvalidHistoryFrequencyError(frequency=freq)
|
||||||
|
|
||||||
Parameters
|
if unit.lower() == 'd':
|
||||||
----------
|
timeframe = '{}d'.format(candle_size)
|
||||||
timeframe
|
|
||||||
|
|
||||||
Returns
|
elif unit.lower() == 'm' or unit == 'T':
|
||||||
-------
|
timeframe = '{}m'.format(candle_size)
|
||||||
|
|
||||||
"""
|
elif unit.lower() == 'h' or unit == 'T':
|
||||||
return CCXT.map_frequency(
|
timeframe = '{}h'.format(candle_size)
|
||||||
timeframe, source='ccxt', raise_error=raise_error
|
|
||||||
)
|
return timeframe
|
||||||
|
|
||||||
def get_candles(self, freq, assets, bar_count=None, start_dt=None,
|
def get_candles(self, freq, assets, bar_count=None, start_dt=None,
|
||||||
end_dt=None):
|
end_dt=None):
|
||||||
is_single = (isinstance(assets, TradingPair))
|
|
||||||
if is_single:
|
|
||||||
assets = [assets]
|
|
||||||
|
|
||||||
symbols = self.get_symbols(assets)
|
symbols = self.get_symbols(assets)
|
||||||
timeframe = CCXT.get_timeframe(freq)
|
timeframe = self.get_timeframe(freq)
|
||||||
|
delta = start_dt - pd.to_datetime('1970-1-1', utc=True)
|
||||||
ms = None
|
ms = int(delta.total_seconds()) * 1000
|
||||||
if start_dt is not None:
|
|
||||||
delta = start_dt - get_epoch()
|
|
||||||
ms = int(delta.total_seconds()) * 1000
|
|
||||||
|
|
||||||
candles = dict()
|
candles = dict()
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
@@ -389,9 +200,7 @@ class CCXT(Exchange):
|
|||||||
candles[asset] = []
|
candles[asset] = []
|
||||||
for ohlcv in ohlcvs:
|
for ohlcv in ohlcvs:
|
||||||
candles[asset].append(dict(
|
candles[asset].append(dict(
|
||||||
last_traded=pd.to_datetime(
|
last_traded=pd.to_datetime(ohlcv[0], unit='ms', utc=True),
|
||||||
ohlcv[0], unit='ms', utc=True
|
|
||||||
),
|
|
||||||
open=ohlcv[1],
|
open=ohlcv[1],
|
||||||
high=ohlcv[2],
|
high=ohlcv[2],
|
||||||
low=ohlcv[3],
|
low=ohlcv[3],
|
||||||
@@ -399,11 +208,7 @@ class CCXT(Exchange):
|
|||||||
volume=ohlcv[5]
|
volume=ohlcv[5]
|
||||||
))
|
))
|
||||||
|
|
||||||
if is_single:
|
return candles
|
||||||
return six.next(six.itervalues(candles))
|
|
||||||
|
|
||||||
else:
|
|
||||||
return candles
|
|
||||||
|
|
||||||
def _fetch_symbol_map(self, is_local):
|
def _fetch_symbol_map(self, is_local):
|
||||||
try:
|
try:
|
||||||
@@ -469,7 +274,7 @@ class CCXT(Exchange):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def create_trading_pair(self, market, asset_def=None, is_local=False):
|
def create_trading_pair(self, market, asset_def, is_local):
|
||||||
"""
|
"""
|
||||||
Creating a TradingPair from market and asset data.
|
Creating a TradingPair from market and asset data.
|
||||||
|
|
||||||
@@ -515,24 +320,18 @@ class CCXT(Exchange):
|
|||||||
and asset_def['end_minute'] != 'N/A' else None
|
and asset_def['end_minute'] != 'N/A' else None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
params['symbol'] = get_catalyst_symbol(market)
|
params['symbol'] = self.get_catalyst_symbol(market)
|
||||||
# TODO: add as an optional column
|
# TODO: add as an optional column
|
||||||
params['leverage'] = 1.0
|
params['leverage'] = 1.0
|
||||||
|
|
||||||
return TradingPair(**params)
|
return TradingPair(**params)
|
||||||
|
|
||||||
def load_assets(self):
|
def load_assets(self):
|
||||||
log.debug('loading assets for {}'.format(self.name))
|
|
||||||
self.assets = []
|
self.assets = []
|
||||||
|
|
||||||
for market in self.markets:
|
for market in self.markets:
|
||||||
if 'id' not in market:
|
|
||||||
log.warn('invalid market: {}'.format(market))
|
|
||||||
continue
|
|
||||||
|
|
||||||
asset_defs = self.get_asset_defs(market)
|
asset_defs = self.get_asset_defs(market)
|
||||||
|
|
||||||
asset = None
|
|
||||||
for asset_def in asset_defs:
|
for asset_def in asset_defs:
|
||||||
if asset_def[0] is not None or not asset_defs[1]:
|
if asset_def[0] is not None or not asset_defs[1]:
|
||||||
try:
|
try:
|
||||||
@@ -543,12 +342,8 @@ class CCXT(Exchange):
|
|||||||
)
|
)
|
||||||
self.assets.append(asset)
|
self.assets.append(asset)
|
||||||
|
|
||||||
except TypeError as e:
|
except TypeError:
|
||||||
log.warn('unable to add asset: {}'.format(e))
|
pass
|
||||||
|
|
||||||
if asset is None:
|
|
||||||
asset = self.create_trading_pair(market=market)
|
|
||||||
self.assets.append(asset)
|
|
||||||
|
|
||||||
def get_balances(self):
|
def get_balances(self):
|
||||||
try:
|
try:
|
||||||
@@ -580,61 +375,21 @@ class CCXT(Exchange):
|
|||||||
The Catalyst order object
|
The Catalyst order object
|
||||||
|
|
||||||
"""
|
"""
|
||||||
order_id = order_status['id']
|
if order_status['status'] == 'canceled':
|
||||||
symbol = self.get_symbol(order_status['symbol'], source='ccxt')
|
|
||||||
asset = self.get_asset(symbol)
|
|
||||||
|
|
||||||
s = order_status['status']
|
|
||||||
amount = order_status['amount']
|
|
||||||
filled = order_status['filled']
|
|
||||||
|
|
||||||
if s == 'canceled' or (s == 'closed' and filled == 0):
|
|
||||||
status = ORDER_STATUS.CANCELLED
|
status = ORDER_STATUS.CANCELLED
|
||||||
|
|
||||||
elif s == 'closed' and filled > 0:
|
elif order_status['status'] == 'closed' and order_status['filled'] > 0:
|
||||||
if filled < amount:
|
log.debug('found executed order {}'.format(order_status))
|
||||||
log.warn(
|
|
||||||
'order {id} is executed but only partially filled:'
|
|
||||||
' {filled} {symbol} out of {amount}'.format(
|
|
||||||
id=order_status['status'],
|
|
||||||
filled=order_status['filled'],
|
|
||||||
symbol=asset.symbol,
|
|
||||||
amount=order_status['amount'],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log.info(
|
|
||||||
'order {id} executed in full: {filled} {symbol}'.format(
|
|
||||||
id=order_id,
|
|
||||||
filled=filled,
|
|
||||||
symbol=asset.symbol,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
status = ORDER_STATUS.FILLED
|
status = ORDER_STATUS.FILLED
|
||||||
|
|
||||||
elif s == 'open':
|
elif order_status['status'] == 'open':
|
||||||
status = ORDER_STATUS.OPEN
|
|
||||||
|
|
||||||
elif filled > 0:
|
|
||||||
log.info(
|
|
||||||
'order {id} partially filled: {filled} {symbol} out of '
|
|
||||||
'{amount}, waiting for complete execution'.format(
|
|
||||||
id=order_id,
|
|
||||||
filled=filled,
|
|
||||||
symbol=asset.symbol,
|
|
||||||
amount=amount,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
status = ORDER_STATUS.OPEN
|
status = ORDER_STATUS.OPEN
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.warn(
|
raise ValueError('invalid state for order')
|
||||||
'invalid state {} for order {}'.format(
|
|
||||||
s, order_id
|
amount = order_status['amount']
|
||||||
)
|
filled = order_status['filled']
|
||||||
)
|
|
||||||
status = ORDER_STATUS.OPEN
|
|
||||||
|
|
||||||
if order_status['side'] == 'sell':
|
if order_status['side'] == 'sell':
|
||||||
amount = -amount
|
amount = -amount
|
||||||
@@ -644,16 +399,25 @@ class CCXT(Exchange):
|
|||||||
order_type = order_status['type']
|
order_type = order_status['type']
|
||||||
|
|
||||||
limit_price = price if order_type == 'limit' else None
|
limit_price = price if order_type == 'limit' else None
|
||||||
|
stop_price = None # TODO: add support
|
||||||
|
|
||||||
executed_price = order_status['cost'] / order_status['amount']
|
executed_price = order_status['cost'] / order_status['amount']
|
||||||
commission = order_status['fee']
|
commission = order_status['fee']
|
||||||
date = from_ms_timestamp(order_status['timestamp'])
|
date = from_ms_timestamp(order_status['timestamp'])
|
||||||
|
|
||||||
|
# order_id = str(order_status['info']['clientOrderId'])
|
||||||
|
order_id = order_status['id']
|
||||||
|
|
||||||
|
# TODO: this won't work, redo the packages with a different key.
|
||||||
|
symbol = order_status['info']['symbol'] \
|
||||||
|
if 'symbol' in order_status['info'] \
|
||||||
|
else order_status['info']['Exchange']
|
||||||
|
|
||||||
order = Order(
|
order = Order(
|
||||||
dt=date,
|
dt=date,
|
||||||
asset=asset,
|
asset=self.get_asset(symbol, is_exchange_symbol=True),
|
||||||
amount=amount,
|
amount=amount,
|
||||||
stop=None,
|
stop=stop_price,
|
||||||
limit=limit_price,
|
limit=limit_price,
|
||||||
filled=filled,
|
filled=filled,
|
||||||
id=order_id,
|
id=order_id,
|
||||||
@@ -681,45 +445,27 @@ class CCXT(Exchange):
|
|||||||
)
|
)
|
||||||
|
|
||||||
side = 'buy' if amount > 0 else 'sell'
|
side = 'buy' if amount > 0 else 'sell'
|
||||||
if hasattr(self.api, 'amount_to_lots'):
|
|
||||||
adj_amount = self.api.amount_to_lots(
|
|
||||||
symbol=symbol,
|
|
||||||
amount=abs(amount),
|
|
||||||
)
|
|
||||||
if adj_amount != abs(amount):
|
|
||||||
log.info(
|
|
||||||
'adjusted order amount {} to {} based on lot size'.format(
|
|
||||||
abs(amount), adj_amount,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
adj_amount = abs(amount)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.api.create_order(
|
result = self.api.create_order(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
type=order_type,
|
type=order_type,
|
||||||
side=side,
|
side=side,
|
||||||
amount=adj_amount,
|
amount=abs(amount),
|
||||||
price=price
|
price=price
|
||||||
)
|
)
|
||||||
except ExchangeNotAvailable as e:
|
except ExchangeNotAvailable as e:
|
||||||
log.debug('unable to create order: {}'.format(e))
|
log.debug('unable to create order: {}'.format(e))
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
except InvalidOrder as e:
|
|
||||||
log.warn('the exchange rejected the order: {}'.format(e))
|
|
||||||
raise CreateOrderError(exchange=self.name, error=e)
|
|
||||||
|
|
||||||
if 'info' not in result:
|
if 'info' not in result:
|
||||||
raise ValueError('cannot use order without info attribute')
|
raise ValueError('cannot use order without info attribute')
|
||||||
|
|
||||||
final_amount = adj_amount if side == 'buy' else -adj_amount
|
|
||||||
order_id = result['id']
|
order_id = result['id']
|
||||||
order = Order(
|
order = Order(
|
||||||
dt=pd.Timestamp.utcnow(),
|
dt=pd.Timestamp.utcnow(),
|
||||||
asset=asset,
|
asset=asset,
|
||||||
amount=final_amount,
|
amount=amount,
|
||||||
stop=style.get_stop_price(is_buy),
|
stop=style.get_stop_price(is_buy),
|
||||||
limit=style.get_limit_price(is_buy),
|
limit=style.get_limit_price(is_buy),
|
||||||
id=order_id
|
id=order_id
|
||||||
@@ -746,7 +492,18 @@ class CCXT(Exchange):
|
|||||||
|
|
||||||
return orders
|
return orders
|
||||||
|
|
||||||
|
def _get_asset_from_order(self, order_id):
|
||||||
|
open_orders = self.portfolio.open_orders
|
||||||
|
order = next(
|
||||||
|
(open_orders[id] for id in open_orders if id == order_id),
|
||||||
|
None
|
||||||
|
) # type: Order
|
||||||
|
return order.asset if order is not None else None
|
||||||
|
|
||||||
def get_order(self, order_id, asset_or_symbol=None):
|
def get_order(self, order_id, asset_or_symbol=None):
|
||||||
|
if asset_or_symbol is None and self.portfolio is not None:
|
||||||
|
asset_or_symbol = self._get_asset_from_order(order_id)
|
||||||
|
|
||||||
if asset_or_symbol is None:
|
if asset_or_symbol is None:
|
||||||
log.debug(
|
log.debug(
|
||||||
'order not found in memory, the request might fail '
|
'order not found in memory, the request might fail '
|
||||||
@@ -767,6 +524,9 @@ class CCXT(Exchange):
|
|||||||
order_id = order_param.id \
|
order_id = order_param.id \
|
||||||
if isinstance(order_param, Order) else order_param
|
if isinstance(order_param, Order) else order_param
|
||||||
|
|
||||||
|
if asset_or_symbol is None and self.portfolio is not None:
|
||||||
|
asset_or_symbol = self._get_asset_from_order(order_id)
|
||||||
|
|
||||||
if asset_or_symbol is None:
|
if asset_or_symbol is None:
|
||||||
log.debug(
|
log.debug(
|
||||||
'order not found in memory, cancelling order might fail '
|
'order not found in memory, cancelling order might fail '
|
||||||
@@ -794,47 +554,21 @@ class CCXT(Exchange):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
tickers = dict()
|
tickers = dict()
|
||||||
try:
|
for asset in assets:
|
||||||
for asset in assets:
|
ccxt_symbol = self.get_symbol(asset)
|
||||||
symbol = self.get_symbol(asset)
|
ticker = self.api.fetch_ticker(ccxt_symbol)
|
||||||
# TODO: use fetch_tickers() for efficiency
|
|
||||||
# I tried using fetch_tickers() but noticed some
|
|
||||||
# inconsistencies, see issue:
|
|
||||||
# https://github.com/ccxt/ccxt/issues/870
|
|
||||||
ticker = self.api.fetch_ticker(symbol=symbol)
|
|
||||||
if not ticker:
|
|
||||||
log.warn('ticker not found for {} {}'.format(
|
|
||||||
self.name, symbol
|
|
||||||
))
|
|
||||||
continue
|
|
||||||
|
|
||||||
ticker['last_traded'] = from_ms_timestamp(ticker['timestamp'])
|
ticker['last_traded'] = from_ms_timestamp(ticker['timestamp'])
|
||||||
|
|
||||||
if 'last_price' not in ticker:
|
if 'last_price' not in ticker:
|
||||||
# TODO: any more exceptions?
|
# TODO: any more exceptions?
|
||||||
ticker['last_price'] = ticker['last']
|
ticker['last_price'] = ticker['last']
|
||||||
|
|
||||||
if 'baseVolume' in ticker and ticker['baseVolume'] is not None:
|
# Using the volume represented in the base currency
|
||||||
# Using the volume represented in the base currency
|
ticker['volume'] = ticker['baseVolume'] \
|
||||||
ticker['volume'] = ticker['baseVolume']
|
if 'baseVolume' in ticker else 0
|
||||||
|
|
||||||
elif 'info' in ticker and 'bidQty' in ticker['info'] \
|
tickers[asset] = ticker
|
||||||
and 'askQty' in ticker['info']:
|
|
||||||
ticker['volume'] = float(ticker['info']['bidQty']) + \
|
|
||||||
float(ticker['info']['askQty'])
|
|
||||||
|
|
||||||
else:
|
|
||||||
ticker['volume'] = 0
|
|
||||||
|
|
||||||
tickers[asset] = ticker
|
|
||||||
|
|
||||||
except ExchangeNotAvailable as e:
|
|
||||||
log.warn(
|
|
||||||
'unable to fetch ticker: {} {}'.format(
|
|
||||||
self.name, asset.symbol
|
|
||||||
)
|
|
||||||
)
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
return tickers
|
return tickers
|
||||||
|
|
||||||
|
|||||||
+155
-169
@@ -5,20 +5,26 @@ from time import sleep
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst.algorithm import MarketOrder
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.data.data_portal import BASE_FIELDS
|
from catalyst.data.data_portal import BASE_FIELDS
|
||||||
|
from catalyst.exchange.bundle_utils import get_start_dt, \
|
||||||
|
get_delta, get_periods, get_periods_range
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
|
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
|
||||||
SymbolNotFoundOnExchange, \
|
BaseCurrencyNotFoundError, SymbolNotFoundOnExchange, \
|
||||||
PricingDataNotLoadedError, \
|
PricingDataNotLoadedError, \
|
||||||
NoDataAvailableOnExchange, NoValueForField, LastCandleTooEarlyError, \
|
NoDataAvailableOnExchange, NoValueForField
|
||||||
TickerNotFoundError, NotEnoughCashError
|
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
|
||||||
from catalyst.exchange.utils.bundle_utils import get_start_dt, \
|
ExchangeLimitOrder, ExchangeStopOrder
|
||||||
get_delta, get_periods, get_periods_range
|
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_symbols, \
|
from catalyst.exchange.exchange_utils import get_exchange_symbols, \
|
||||||
get_frequency, resample_history_df, has_bundle
|
get_frequency, resample_history_df
|
||||||
|
from catalyst.finance.order import ORDER_STATUS
|
||||||
|
from catalyst.finance.transaction import Transaction
|
||||||
|
|
||||||
log = Logger('Exchange', level=LOG_LEVEL)
|
log = Logger('Exchange', level=LOG_LEVEL)
|
||||||
|
|
||||||
@@ -30,6 +36,7 @@ class Exchange:
|
|||||||
self.name = None
|
self.name = None
|
||||||
self.assets = []
|
self.assets = []
|
||||||
self._symbol_maps = [None, None]
|
self._symbol_maps = [None, None]
|
||||||
|
self._portfolio = None
|
||||||
self.minute_writer = None
|
self.minute_writer = None
|
||||||
self.minute_reader = None
|
self.minute_reader = None
|
||||||
self.base_currency = None
|
self.base_currency = None
|
||||||
@@ -39,7 +46,26 @@ class Exchange:
|
|||||||
self.request_cpt = None
|
self.request_cpt = None
|
||||||
self.bundle = ExchangeBundle(self.name)
|
self.bundle = ExchangeBundle(self.name)
|
||||||
|
|
||||||
self.low_balance_threshold = None
|
@property
|
||||||
|
def positions(self):
|
||||||
|
return self.portfolio.positions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def portfolio(self):
|
||||||
|
"""
|
||||||
|
The exchange portfolio
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ExchangePortfolio
|
||||||
|
"""
|
||||||
|
if self._portfolio is None:
|
||||||
|
self._portfolio = ExchangePortfolio(
|
||||||
|
start_date=pd.Timestamp.utcnow()
|
||||||
|
)
|
||||||
|
self.synchronize_portfolio()
|
||||||
|
|
||||||
|
return self._portfolio
|
||||||
|
|
||||||
@abstractproperty
|
@abstractproperty
|
||||||
def account(self):
|
def account(self):
|
||||||
@@ -49,9 +75,6 @@ class Exchange:
|
|||||||
def time_skew(self):
|
def time_skew(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def has_bundle(self, data_frequency):
|
|
||||||
return has_bundle(self.name, data_frequency)
|
|
||||||
|
|
||||||
def is_open(self, dt):
|
def is_open(self, dt):
|
||||||
"""
|
"""
|
||||||
Is the exchange open
|
Is the exchange open
|
||||||
@@ -154,7 +177,7 @@ class Exchange:
|
|||||||
|
|
||||||
def get_assets(self, symbols=None, data_frequency=None,
|
def get_assets(self, symbols=None, data_frequency=None,
|
||||||
is_exchange_symbol=False,
|
is_exchange_symbol=False,
|
||||||
is_local=None, quote_currency=None):
|
is_local=None):
|
||||||
"""
|
"""
|
||||||
The list of markets for the specified symbols.
|
The list of markets for the specified symbols.
|
||||||
|
|
||||||
@@ -178,30 +201,14 @@ class Exchange:
|
|||||||
if symbols is None:
|
if symbols is None:
|
||||||
# Make a distinct list of all symbols
|
# Make a distinct list of all symbols
|
||||||
symbols = list(set([asset.symbol for asset in self.assets]))
|
symbols = list(set([asset.symbol for asset in self.assets]))
|
||||||
|
|
||||||
if quote_currency is not None:
|
|
||||||
for symbol in symbols[:]:
|
|
||||||
suffix = '_{}'.format(quote_currency.lower())
|
|
||||||
|
|
||||||
if not symbol.endswith(suffix):
|
|
||||||
symbols.remove(symbol)
|
|
||||||
|
|
||||||
is_exchange_symbol = False
|
is_exchange_symbol = False
|
||||||
|
|
||||||
assets = []
|
assets = []
|
||||||
for symbol in symbols:
|
for symbol in symbols:
|
||||||
try:
|
asset = self.get_asset(
|
||||||
asset = self.get_asset(
|
symbol, data_frequency, is_exchange_symbol, is_local
|
||||||
symbol, data_frequency, is_exchange_symbol, is_local
|
)
|
||||||
)
|
assets.append(asset)
|
||||||
assets.append(asset)
|
|
||||||
|
|
||||||
except SymbolNotFoundOnExchange:
|
|
||||||
log.debug(
|
|
||||||
'skipping non-existent market {} {}'.format(
|
|
||||||
self.name, symbol
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return assets
|
return assets
|
||||||
|
|
||||||
def get_asset(self, symbol, data_frequency=None, is_exchange_symbol=False,
|
def get_asset(self, symbol, data_frequency=None, is_exchange_symbol=False,
|
||||||
@@ -249,10 +256,8 @@ class Exchange:
|
|||||||
|
|
||||||
elif data_frequency is not None:
|
elif data_frequency is not None:
|
||||||
applies = (
|
applies = (
|
||||||
(
|
(data_frequency == 'minute' and a.end_minute is not None)
|
||||||
data_frequency == 'minute' and a.end_minute is not None)
|
or (data_frequency == 'daily' and a.end_daily is not None)
|
||||||
or (
|
|
||||||
data_frequency == 'daily' and a.end_daily is not None)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -261,19 +266,13 @@ class Exchange:
|
|||||||
# The symbol provided may use the Catalyst or the exchange
|
# The symbol provided may use the Catalyst or the exchange
|
||||||
# convention
|
# convention
|
||||||
key = a.exchange_symbol if is_exchange_symbol else a.symbol
|
key = a.exchange_symbol if is_exchange_symbol else a.symbol
|
||||||
if not asset and key.lower() == symbol.lower():
|
if not asset and key.lower() == symbol.lower() and applies:
|
||||||
if applies:
|
asset = a
|
||||||
asset = a
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise NoDataAvailableOnExchange(
|
|
||||||
symbol=key,
|
|
||||||
exchange=self.name,
|
|
||||||
data_frequency=data_frequency,
|
|
||||||
)
|
|
||||||
|
|
||||||
if asset is None:
|
if asset is None:
|
||||||
supported_symbols = sorted([a.symbol for a in self.assets])
|
supported_symbols = sorted([
|
||||||
|
asset.symbol for asset in self.assets
|
||||||
|
])
|
||||||
|
|
||||||
raise SymbolNotFoundOnExchange(
|
raise SymbolNotFoundOnExchange(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
@@ -294,16 +293,6 @@ class Exchange:
|
|||||||
self._symbol_maps[index] = symbol_map
|
self._symbol_maps[index] = symbol_map
|
||||||
return symbol_map
|
return symbol_map
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def init(self):
|
|
||||||
"""
|
|
||||||
Load the asset list from the network.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def load_assets(self, is_local=False):
|
def load_assets(self, is_local=False):
|
||||||
"""
|
"""
|
||||||
@@ -324,6 +313,54 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def check_open_orders(self):
|
||||||
|
"""
|
||||||
|
Loop through the list of open orders in the Portfolio object.
|
||||||
|
For each executed order found, create a transaction and apply to the
|
||||||
|
Portfolio.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[Transaction]
|
||||||
|
|
||||||
|
"""
|
||||||
|
transactions = list()
|
||||||
|
if self.portfolio.open_orders:
|
||||||
|
for order_id in list(self.portfolio.open_orders):
|
||||||
|
log.debug('found open order: {}'.format(order_id))
|
||||||
|
|
||||||
|
order, executed_price = self.get_order(order_id)
|
||||||
|
log.debug(
|
||||||
|
'got updated order {} {}'.format(
|
||||||
|
order, executed_price
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if order.status == ORDER_STATUS.FILLED:
|
||||||
|
transaction = Transaction(
|
||||||
|
asset=order.asset,
|
||||||
|
amount=order.amount,
|
||||||
|
dt=pd.Timestamp.utcnow(),
|
||||||
|
price=executed_price,
|
||||||
|
order_id=order.id,
|
||||||
|
commission=order.commission
|
||||||
|
)
|
||||||
|
transactions.append(transaction)
|
||||||
|
|
||||||
|
self.portfolio.execute_order(order, transaction)
|
||||||
|
|
||||||
|
elif order.status == ORDER_STATUS.CANCELLED:
|
||||||
|
self.portfolio.remove_order(order)
|
||||||
|
|
||||||
|
else:
|
||||||
|
delta = pd.Timestamp.utcnow() - order.dt
|
||||||
|
log.info(
|
||||||
|
'order {order_id} still open after {delta}'.format(
|
||||||
|
order_id=order_id,
|
||||||
|
delta=delta
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return transactions
|
||||||
|
|
||||||
def get_spot_value(self, assets, field, dt=None, data_frequency='minute'):
|
def get_spot_value(self, assets, field, dt=None, data_frequency='minute'):
|
||||||
"""
|
"""
|
||||||
Public API method that returns a scalar value representing the value
|
Public API method that returns a scalar value representing the value
|
||||||
@@ -409,7 +446,6 @@ class Exchange:
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# TODO: replace with catalyst.exchange.exchange_utils.get_candles_df
|
|
||||||
def get_series_from_candles(self, candles, start_dt, end_dt,
|
def get_series_from_candles(self, candles, start_dt, end_dt,
|
||||||
data_frequency, field, previous_value=None):
|
data_frequency, field, previous_value=None):
|
||||||
"""
|
"""
|
||||||
@@ -442,7 +478,7 @@ class Exchange:
|
|||||||
method='ffill',
|
method='ffill',
|
||||||
fill_value=previous_value,
|
fill_value=previous_value,
|
||||||
)
|
)
|
||||||
series.sort_index(inplace=True)
|
|
||||||
return series
|
return series
|
||||||
|
|
||||||
def get_history_window(self,
|
def get_history_window(self,
|
||||||
@@ -452,7 +488,7 @@ class Exchange:
|
|||||||
frequency,
|
frequency,
|
||||||
field,
|
field,
|
||||||
data_frequency=None,
|
data_frequency=None,
|
||||||
is_current=False):
|
ffill=True):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Public API method that returns a dataframe containing the requested
|
Public API method that returns a dataframe containing the requested
|
||||||
@@ -479,15 +515,10 @@ class Exchange:
|
|||||||
The frequency of the data to query; i.e. whether the data is
|
The frequency of the data to query; i.e. whether the data is
|
||||||
'daily' or 'minute' bars.
|
'daily' or 'minute' bars.
|
||||||
|
|
||||||
is_current: bool
|
# TODO: fill how?
|
||||||
Skip date filters when current data is requested (last few bars
|
ffill: boolean
|
||||||
until now).
|
Forward-fill missing values. Only has effect if field
|
||||||
|
is 'price'.
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
Catalysts requires an end data with bar count both CCXT wants a
|
|
||||||
start data with bar count. Since we have to make calculations here,
|
|
||||||
we ensure that the last candle match the end_dt parameter.
|
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -499,7 +530,6 @@ class Exchange:
|
|||||||
frequency, data_frequency
|
frequency, data_frequency
|
||||||
)
|
)
|
||||||
adj_bar_count = candle_size * bar_count
|
adj_bar_count = candle_size * bar_count
|
||||||
|
|
||||||
start_dt = get_start_dt(end_dt, adj_bar_count, data_frequency)
|
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
|
||||||
@@ -507,8 +537,8 @@ class Exchange:
|
|||||||
freq=freq,
|
freq=freq,
|
||||||
assets=assets,
|
assets=assets,
|
||||||
bar_count=bar_count,
|
bar_count=bar_count,
|
||||||
start_dt=start_dt if not is_current else None,
|
start_dt=start_dt,
|
||||||
end_dt=end_dt if not is_current else None,
|
end_dt=end_dt
|
||||||
)
|
)
|
||||||
|
|
||||||
series = dict()
|
series = dict()
|
||||||
@@ -520,17 +550,6 @@ class Exchange:
|
|||||||
data_frequency=frequency,
|
data_frequency=frequency,
|
||||||
field=field,
|
field=field,
|
||||||
)
|
)
|
||||||
if end_dt is not None:
|
|
||||||
delta = get_delta(candle_size, data_frequency)
|
|
||||||
adj_end_dt = end_dt - delta
|
|
||||||
last_traded = asset_series.index[-1]
|
|
||||||
|
|
||||||
if last_traded < adj_end_dt:
|
|
||||||
raise LastCandleTooEarlyError(
|
|
||||||
last_traded=last_traded,
|
|
||||||
end_dt=adj_end_dt,
|
|
||||||
exchange=self.name,
|
|
||||||
)
|
|
||||||
series[asset] = asset_series
|
series[asset] = asset_series
|
||||||
|
|
||||||
df = pd.DataFrame(series)
|
df = pd.DataFrame(series)
|
||||||
@@ -588,7 +607,6 @@ class Exchange:
|
|||||||
frequency, data_frequency
|
frequency, data_frequency
|
||||||
)
|
)
|
||||||
adj_bar_count = candle_size * bar_count
|
adj_bar_count = candle_size * bar_count
|
||||||
|
|
||||||
try:
|
try:
|
||||||
series = self.bundle.get_history_window_series_and_load(
|
series = self.bundle.get_history_window_series_and_load(
|
||||||
assets=assets,
|
assets=assets,
|
||||||
@@ -598,7 +616,6 @@ class Exchange:
|
|||||||
data_frequency=data_frequency,
|
data_frequency=data_frequency,
|
||||||
force_auto_ingest=force_auto_ingest
|
force_auto_ingest=force_auto_ingest
|
||||||
)
|
)
|
||||||
|
|
||||||
except (PricingDataNotLoadedError, NoDataAvailableOnExchange):
|
except (PricingDataNotLoadedError, NoDataAvailableOnExchange):
|
||||||
series = dict()
|
series = dict()
|
||||||
|
|
||||||
@@ -652,100 +669,51 @@ class Exchange:
|
|||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def _check_low_balance(self, currency, balances, amount):
|
def synchronize_portfolio(self):
|
||||||
free = balances[currency]['free'] if currency in balances else 0.0
|
|
||||||
|
|
||||||
if free < amount:
|
|
||||||
return free, True
|
|
||||||
|
|
||||||
else:
|
|
||||||
return free, False
|
|
||||||
|
|
||||||
def sync_positions(self, positions, cash=None, check_balances=False):
|
|
||||||
"""
|
"""
|
||||||
Update the portfolio cash and position balances based on the
|
Update the portfolio cash and position balances based on the
|
||||||
latest ticker prices.
|
latest ticker prices.
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
positions:
|
|
||||||
The positions to synchronize.
|
|
||||||
|
|
||||||
check_balances:
|
|
||||||
Check balances amounts against the exchange.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
free_cash = 0.0
|
log.debug('synchronizing portfolio with exchange {}'.format(self.name))
|
||||||
if check_balances:
|
balances = self.get_balances()
|
||||||
log.debug('fetching {} balances'.format(self.name))
|
|
||||||
balances = self.get_balances()
|
|
||||||
log.debug(
|
|
||||||
'got free balances for {} currencies'.format(
|
|
||||||
len(balances)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if cash is not None:
|
|
||||||
free_cash, is_lower = self._check_low_balance(
|
|
||||||
currency=self.base_currency,
|
|
||||||
balances=balances,
|
|
||||||
amount=cash,
|
|
||||||
)
|
|
||||||
if is_lower:
|
|
||||||
raise NotEnoughCashError(
|
|
||||||
currency=self.base_currency,
|
|
||||||
exchange=self.name,
|
|
||||||
free=free_cash,
|
|
||||||
cash=cash,
|
|
||||||
)
|
|
||||||
|
|
||||||
positions_value = 0.0
|
base_position_available = balances[self.base_currency]['free'] \
|
||||||
if positions is not None:
|
if self.base_currency in balances else None
|
||||||
assets = set([position.asset for position in positions])
|
|
||||||
|
if base_position_available is None:
|
||||||
|
raise BaseCurrencyNotFoundError(
|
||||||
|
base_currency=self.base_currency,
|
||||||
|
exchange=self.name.title()
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio = self._portfolio
|
||||||
|
portfolio.cash = base_position_available
|
||||||
|
log.debug('found base currency balance: {}'.format(portfolio.cash))
|
||||||
|
|
||||||
|
if portfolio.starting_cash is None:
|
||||||
|
portfolio.starting_cash = portfolio.cash
|
||||||
|
|
||||||
|
if portfolio.positions:
|
||||||
|
assets = list(portfolio.positions.keys())
|
||||||
tickers = self.tickers(assets)
|
tickers = self.tickers(assets)
|
||||||
|
|
||||||
for position in positions:
|
portfolio.positions_value = 0.0
|
||||||
asset = position.asset
|
for asset in tickers:
|
||||||
if asset not in tickers:
|
# TODO: convert if the position is not in the base currency
|
||||||
raise TickerNotFoundError(
|
|
||||||
symbol=asset.symbol,
|
|
||||||
exchange=self.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
ticker = tickers[asset]
|
ticker = tickers[asset]
|
||||||
log.debug(
|
position = portfolio.positions[asset]
|
||||||
'updating {symbol} position, last traded on {dt} for '
|
|
||||||
'{price}{currency}'.format(
|
|
||||||
symbol=asset.symbol,
|
|
||||||
dt=ticker['last_traded'],
|
|
||||||
price=ticker['last_price'],
|
|
||||||
currency=asset.quote_currency,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
position.last_sale_price = ticker['last_price']
|
position.last_sale_price = ticker['last_price']
|
||||||
position.last_sale_date = ticker['last_traded']
|
position.last_sale_date = ticker['last_traded']
|
||||||
|
|
||||||
positions_value += \
|
portfolio.positions_value += \
|
||||||
position.amount * position.last_sale_price
|
position.amount * position.last_sale_price
|
||||||
|
portfolio.portfolio_value = \
|
||||||
|
portfolio.positions_value + portfolio.cash
|
||||||
|
|
||||||
if check_balances:
|
def order(self, asset, amount, limit_price=None, stop_price=None,
|
||||||
free, is_lower = self._check_low_balance(
|
style=None):
|
||||||
currency=asset.base_currency,
|
|
||||||
balances=balances,
|
|
||||||
amount=position.amount,
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_lower:
|
|
||||||
log.warn(
|
|
||||||
'detected lower balance for {} on {}: {} < {}, '
|
|
||||||
'updating position amount'.format(
|
|
||||||
asset.symbol, self.name, free, position.amount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
position.amount = free
|
|
||||||
|
|
||||||
return free_cash, positions_value
|
|
||||||
|
|
||||||
def order(self, asset, amount, style):
|
|
||||||
"""Place an order.
|
"""Place an order.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -804,11 +772,24 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
|
|
||||||
is_buy = (amount > 0)
|
is_buy = (amount > 0)
|
||||||
display_price = style.get_limit_price(is_buy)
|
|
||||||
|
|
||||||
|
if limit_price is not None and stop_price is not None:
|
||||||
|
style = ExchangeStopLimitOrder(
|
||||||
|
limit_price, stop_price, exchange=self.name
|
||||||
|
)
|
||||||
|
|
||||||
|
elif limit_price is not None:
|
||||||
|
style = ExchangeLimitOrder(limit_price, exchange=self.name)
|
||||||
|
|
||||||
|
elif stop_price is not None:
|
||||||
|
style = ExchangeStopOrder(stop_price, exchange=self.name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
style = MarketOrder(exchange=self.name)
|
||||||
|
|
||||||
|
display_price = limit_price if limit_price is not None else stop_price
|
||||||
log.debug(
|
log.debug(
|
||||||
'issuing {side} order of {amount} {symbol} for {type}:'
|
'issuing {side} order of {amount} {symbol} for {type}: {price}'.format(
|
||||||
' {price}'.format(
|
|
||||||
side='buy' if is_buy else 'sell',
|
side='buy' if is_buy else 'sell',
|
||||||
amount=amount,
|
amount=amount,
|
||||||
symbol=asset.symbol,
|
symbol=asset.symbol,
|
||||||
@@ -817,7 +798,12 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.create_order(asset, amount, is_buy, style)
|
order = self.create_order(asset, amount, is_buy, style)
|
||||||
|
if order:
|
||||||
|
self._portfolio.create_order(order)
|
||||||
|
return order.id
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
# The methods below must be implemented for each exchange.
|
# The methods below must be implemented for each exchange.
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -10,43 +10,45 @@
|
|||||||
# 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 copy
|
|
||||||
import pickle
|
import pickle
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
from collections import deque
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from os import listdir
|
from os import listdir
|
||||||
from os.path import isfile, join
|
from os.path import isfile, join
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
import logbook
|
import logbook
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from redo import retry
|
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.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 (
|
||||||
ExchangeRequestError,
|
ExchangeRequestError,
|
||||||
OrderTypeNotSupported)
|
ExchangePortfolioDataError,
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
ExchangeTransactionError,
|
||||||
|
OrphanOrderError)
|
||||||
|
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
|
||||||
|
ExchangeLimitOrder, ExchangeStopOrder
|
||||||
|
from catalyst.exchange.exchange_utils import save_algo_object, get_algo_object, \
|
||||||
|
get_algo_folder, get_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
|
||||||
from catalyst.exchange.utils.exchange_utils import (
|
from catalyst.exchange.stats_utils import get_pretty_stats
|
||||||
save_algo_object,
|
|
||||||
get_algo_object,
|
|
||||||
get_algo_folder,
|
|
||||||
get_algo_df,
|
|
||||||
save_algo_df,
|
|
||||||
group_assets_by_exchange, )
|
|
||||||
from catalyst.exchange.utils.stats_utils import get_pretty_stats, stats_to_s3, \
|
|
||||||
stats_to_algo_folder
|
|
||||||
from catalyst.finance.execution import MarketOrder
|
from catalyst.finance.execution import MarketOrder
|
||||||
from catalyst.finance.performance import PerformanceTracker
|
|
||||||
from catalyst.finance.performance.period import calc_period_stats
|
from catalyst.finance.performance.period import calc_period_stats
|
||||||
from catalyst.gens.tradesimulation import AlgorithmSimulator
|
from catalyst.gens.tradesimulation import AlgorithmSimulator
|
||||||
from catalyst.utils.api_support import api_method
|
from catalyst.utils.api_support import (
|
||||||
from catalyst.utils.input_validation import error_keywords, ensure_upper_case
|
api_method,
|
||||||
|
disallowed_in_before_trading_start)
|
||||||
|
from catalyst.utils.input_validation import error_keywords, ensure_upper_case, \
|
||||||
|
expect_types
|
||||||
from catalyst.utils.math_utils import round_nearest
|
from catalyst.utils.math_utils import round_nearest
|
||||||
from catalyst.utils.preprocess import preprocess
|
from catalyst.utils.preprocess import preprocess
|
||||||
|
|
||||||
@@ -61,104 +63,9 @@ class ExchangeAlgorithmExecutor(AlgorithmSimulator):
|
|||||||
class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.exchanges = kwargs.pop('exchanges', None)
|
self.exchanges = kwargs.pop('exchanges', None)
|
||||||
self.simulate_orders = kwargs.pop('simulate_orders', None)
|
|
||||||
|
|
||||||
super(ExchangeTradingAlgorithmBase, self).__init__(*args, **kwargs)
|
super(ExchangeTradingAlgorithmBase, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.current_day = None
|
|
||||||
|
|
||||||
if self.simulate_orders is None \
|
|
||||||
and self.sim_params.arena == 'backtest':
|
|
||||||
self.simulate_orders = True
|
|
||||||
|
|
||||||
# Operations with retry features
|
|
||||||
self.attempts = dict(
|
|
||||||
get_transactions_attempts=5,
|
|
||||||
order_attempts=5,
|
|
||||||
synchronize_portfolio_attempts=5,
|
|
||||||
get_order_attempts=5,
|
|
||||||
get_open_orders_attempts=5,
|
|
||||||
cancel_order_attempts=5,
|
|
||||||
get_spot_value_attempts=5,
|
|
||||||
get_history_window_attempts=5,
|
|
||||||
retry_sleeptime=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.blotter = ExchangeBlotter(
|
|
||||||
data_frequency=self.data_frequency,
|
|
||||||
# Default to NeverCancel in catalyst
|
|
||||||
cancel_policy=self.cancel_policy,
|
|
||||||
simulate_orders=self.simulate_orders,
|
|
||||||
exchanges=self.exchanges,
|
|
||||||
attempts=self.attempts,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __convert_order_params_for_blotter(limit_price, stop_price, style):
|
|
||||||
"""
|
|
||||||
Helper method for converting deprecated limit_price and stop_price
|
|
||||||
arguments into ExecutionStyle instances.
|
|
||||||
|
|
||||||
This function assumes that either style == None or (limit_price,
|
|
||||||
stop_price) == (None, None).
|
|
||||||
"""
|
|
||||||
if stop_price:
|
|
||||||
raise OrderTypeNotSupported(order_type='stop')
|
|
||||||
|
|
||||||
if style:
|
|
||||||
if limit_price is not None:
|
|
||||||
raise ValueError(
|
|
||||||
'An order style and a limit price was included in the '
|
|
||||||
'order. Please pick one to avoid any possible conflict.'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Currently limiting order types or limit and market to
|
|
||||||
# be in-line with CXXT and many exchanges. We'll consider
|
|
||||||
# adding more order types in the future.
|
|
||||||
if not isinstance(style, ExchangeLimitOrder) or \
|
|
||||||
not isinstance(style, MarketOrder):
|
|
||||||
raise OrderTypeNotSupported(
|
|
||||||
order_type=style.__class__.__name__
|
|
||||||
)
|
|
||||||
|
|
||||||
return style
|
|
||||||
|
|
||||||
if limit_price:
|
|
||||||
return ExchangeLimitOrder(limit_price)
|
|
||||||
else:
|
|
||||||
return MarketOrder()
|
|
||||||
|
|
||||||
@api_method
|
|
||||||
def set_commission(self, maker=None, taker=None):
|
|
||||||
key = self.blotter.commission_models.keys()[0]
|
|
||||||
if maker is not None:
|
|
||||||
self.blotter.commission_models[key].maker = maker
|
|
||||||
|
|
||||||
if taker is not None:
|
|
||||||
self.blotter.commission_models[key].taker = taker
|
|
||||||
|
|
||||||
@api_method
|
|
||||||
def set_slippage(self, spread=None):
|
|
||||||
key = self.blotter.slippage_models.keys()[0]
|
|
||||||
if spread is not None:
|
|
||||||
self.blotter.slippage_models[key].spread = spread
|
|
||||||
|
|
||||||
def _calculate_order(self, asset, amount,
|
|
||||||
limit_price=None, stop_price=None, style=None):
|
|
||||||
# Raises a ZiplineError if invalid parameters are detected.
|
|
||||||
self.validate_order_params(asset,
|
|
||||||
amount,
|
|
||||||
limit_price,
|
|
||||||
stop_price,
|
|
||||||
style)
|
|
||||||
|
|
||||||
# Convert deprecated limit_price and stop_price parameters to use
|
|
||||||
# ExecutionStyle objects.
|
|
||||||
style = self.__convert_order_params_for_blotter(limit_price,
|
|
||||||
stop_price,
|
|
||||||
style)
|
|
||||||
return amount, style
|
|
||||||
|
|
||||||
def round_order(self, amount, asset):
|
def round_order(self, amount, asset):
|
||||||
"""
|
"""
|
||||||
We need fractions with cryptocurrencies
|
We need fractions with cryptocurrencies
|
||||||
@@ -233,28 +140,28 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
tracker = self.perf_tracker
|
tracker = self.perf_tracker
|
||||||
cum = tracker.cumulative_performance
|
period = tracker.todays_performance
|
||||||
|
|
||||||
pos_stats = cum.position_tracker.stats()
|
pos_stats = period.position_tracker.stats()
|
||||||
period_stats = calc_period_stats(pos_stats, cum.ending_cash)
|
period_stats = calc_period_stats(pos_stats, period.ending_cash)
|
||||||
|
|
||||||
stats = dict(
|
stats = dict(
|
||||||
period_start=tracker.period_start,
|
period_start=tracker.period_start,
|
||||||
period_end=tracker.period_end,
|
period_end=tracker.period_end,
|
||||||
capital_base=tracker.capital_base,
|
capital_base=tracker.capital_base,
|
||||||
progress=tracker.progress,
|
progress=tracker.progress,
|
||||||
ending_value=cum.ending_value,
|
ending_value=period.ending_value,
|
||||||
ending_exposure=cum.ending_exposure,
|
ending_exposure=period.ending_exposure,
|
||||||
capital_used=cum.cash_flow,
|
capital_used=period.cash_flow,
|
||||||
starting_value=cum.starting_value,
|
starting_value=period.starting_value,
|
||||||
starting_exposure=cum.starting_exposure,
|
starting_exposure=period.starting_exposure,
|
||||||
starting_cash=cum.starting_cash,
|
starting_cash=period.starting_cash,
|
||||||
ending_cash=cum.ending_cash,
|
ending_cash=period.ending_cash,
|
||||||
portfolio_value=cum.ending_cash + cum.ending_value,
|
portfolio_value=period.ending_cash + period.ending_value,
|
||||||
pnl=cum.pnl,
|
pnl=period.pnl,
|
||||||
returns=cum.returns,
|
returns=period.returns,
|
||||||
period_open=start_dt,
|
period_open=period.period_open,
|
||||||
period_close=end_dt,
|
period_close=period.period_close,
|
||||||
gross_leverage=period_stats.gross_leverage,
|
gross_leverage=period_stats.gross_leverage,
|
||||||
net_leverage=period_stats.net_leverage,
|
net_leverage=period_stats.net_leverage,
|
||||||
short_exposure=pos_stats.short_exposure,
|
short_exposure=pos_stats.short_exposure,
|
||||||
@@ -271,9 +178,8 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
|||||||
# Merging latest recorded variables
|
# Merging latest recorded variables
|
||||||
stats.update(self.recorded_vars)
|
stats.update(self.recorded_vars)
|
||||||
|
|
||||||
stats['positions'] = cum.position_tracker.get_positions_list()
|
stats['positions'] = period.position_tracker.get_positions_list()
|
||||||
|
|
||||||
period = tracker.todays_performance
|
|
||||||
# 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'] = []
|
stats['transactions'] = []
|
||||||
@@ -292,20 +198,56 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
|||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def run(self, data=None, overwrite_sim_params=True):
|
|
||||||
data.attempts = self.attempts
|
|
||||||
return super(ExchangeTradingAlgorithmBase, self).run(
|
|
||||||
data, overwrite_sim_params
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeTradingAlgorithmBacktest(ExchangeTradingAlgorithmBase):
|
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.frame_stats = list()
|
||||||
|
self.blotter = ExchangeBlotter(
|
||||||
|
data_frequency=self.data_frequency,
|
||||||
|
# Default to NeverCancel in catalyst
|
||||||
|
cancel_policy=self.cancel_policy,
|
||||||
|
)
|
||||||
log.info('initialized trading algorithm in backtest mode')
|
log.info('initialized trading algorithm in backtest mode')
|
||||||
|
|
||||||
|
def _calculate_order(self, asset, amount,
|
||||||
|
limit_price=None, stop_price=None, style=None):
|
||||||
|
# Raises a ZiplineError if invalid parameters are detected.
|
||||||
|
self.validate_order_params(asset,
|
||||||
|
amount,
|
||||||
|
limit_price,
|
||||||
|
stop_price,
|
||||||
|
style)
|
||||||
|
|
||||||
|
# Convert deprecated limit_price and stop_price parameters to use
|
||||||
|
# ExecutionStyle objects.
|
||||||
|
style = self.__convert_order_params_for_blotter(limit_price,
|
||||||
|
stop_price,
|
||||||
|
style)
|
||||||
|
return amount, style
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __convert_order_params_for_blotter(limit_price, stop_price, style):
|
||||||
|
"""
|
||||||
|
Helper method for converting deprecated limit_price and stop_price
|
||||||
|
arguments into ExecutionStyle instances.
|
||||||
|
|
||||||
|
This function assumes that either style == None or (limit_price,
|
||||||
|
stop_price) == (None, None).
|
||||||
|
"""
|
||||||
|
if style:
|
||||||
|
assert (limit_price, stop_price) == (None, None)
|
||||||
|
return style
|
||||||
|
if limit_price and stop_price:
|
||||||
|
return ExchangeStopLimitOrder(limit_price, stop_price)
|
||||||
|
if limit_price:
|
||||||
|
return ExchangeLimitOrder(limit_price)
|
||||||
|
if stop_price:
|
||||||
|
return ExchangeStopOrder(stop_price)
|
||||||
|
else:
|
||||||
|
return MarketOrder()
|
||||||
|
|
||||||
def is_last_frame_of_day(self, data):
|
def is_last_frame_of_day(self, data):
|
||||||
# TODO: adjust here to support more intervals
|
# TODO: adjust here to support more intervals
|
||||||
next_frame_dt = data.current_dt + timedelta(minutes=1)
|
next_frame_dt = data.current_dt + timedelta(minutes=1)
|
||||||
@@ -323,8 +265,6 @@ class ExchangeTradingAlgorithmBacktest(ExchangeTradingAlgorithmBase):
|
|||||||
)
|
)
|
||||||
self.frame_stats.append(frame_stats)
|
self.frame_stats.append(frame_stats)
|
||||||
|
|
||||||
self.current_day = data.current_dt.floor('1D')
|
|
||||||
|
|
||||||
def _create_stats_df(self):
|
def _create_stats_df(self):
|
||||||
stats = pd.DataFrame(self.frame_stats)
|
stats = pd.DataFrame(self.frame_stats)
|
||||||
stats.set_index('period_close', inplace=True, drop=False)
|
stats.set_index('period_close', inplace=True, drop=False)
|
||||||
@@ -349,11 +289,10 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.algo_namespace = kwargs.pop('algo_namespace', None)
|
self.algo_namespace = kwargs.pop('algo_namespace', None)
|
||||||
self.live_graph = kwargs.pop('live_graph', None)
|
self.live_graph = kwargs.pop('live_graph', None)
|
||||||
self.stats_output = kwargs.pop('stats_output', None)
|
self.simulate_orders = kwargs.pop('simulate_orders', None)
|
||||||
self._analyze_live = kwargs.pop('analyze_live', None)
|
|
||||||
|
|
||||||
self._clock = None
|
self._clock = None
|
||||||
self.frame_stats = list()
|
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')
|
||||||
|
|
||||||
@@ -365,30 +304,42 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
|
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
|
|
||||||
self.stats_minutes = 1
|
self.retry_check_open_orders = 5
|
||||||
|
self.retry_synchronize_portfolio = 5
|
||||||
|
self.retry_get_open_orders = 5
|
||||||
|
self.retry_order = 2
|
||||||
|
self.retry_delay = 5
|
||||||
|
|
||||||
self._last_orders = []
|
self.stats_minutes = 5
|
||||||
self.trading_client = None
|
|
||||||
|
|
||||||
super(ExchangeTradingAlgorithmLive, self).__init__(*args, **kwargs)
|
super(ExchangeTradingAlgorithmLive, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
try:
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
|
||||||
except ValueError:
|
|
||||||
log.warn("Can't initialize signal handler inside another thread."
|
|
||||||
"Exit should be handled by the user.")
|
|
||||||
|
|
||||||
log.info('initialized trading algorithm in live mode')
|
log.info('initialized trading algorithm in live mode')
|
||||||
|
|
||||||
def interrupt_algorithm(self):
|
def signal_handler(self, signal, frame):
|
||||||
|
"""
|
||||||
|
Handles the keyboard interruption signal.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
signal
|
||||||
|
frame
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
|
|
||||||
if self._analyze is None:
|
if self._analyze is None:
|
||||||
log.info('Exiting the algorithm.')
|
log.info('Interruption signal detected {}, exiting the '
|
||||||
|
'algorithm'.format(signal))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.info('Exiting the algorithm. Calling `analyze()` '
|
log.info('Interruption signal detected {}, calling `analyze()` '
|
||||||
'before exiting the algorithm.')
|
'before exiting the algorithm'.format(signal))
|
||||||
|
|
||||||
algo_folder = get_algo_folder(self.algo_namespace)
|
algo_folder = get_algo_folder(self.algo_namespace)
|
||||||
folder = join(algo_folder, 'daily_perf')
|
folder = join(algo_folder, 'daily_perf')
|
||||||
@@ -406,23 +357,6 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
def signal_handler(self, signal, frame):
|
|
||||||
"""
|
|
||||||
Handles the keyboard interruption signal.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
signal
|
|
||||||
frame
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
log.info('Interruption signal detected {}, exiting the '
|
|
||||||
'algorithm'.format(signal))
|
|
||||||
self.interrupt_algorithm()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def clock(self):
|
def clock(self):
|
||||||
if self._clock is None:
|
if self._clock is None:
|
||||||
@@ -444,14 +378,13 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
|
|
||||||
# This method is taken from TradingAlgorithm.
|
# This method is taken from TradingAlgorithm.
|
||||||
# The clock has been replaced to use RealtimeClock
|
# The clock has been replaced to use RealtimeClock
|
||||||
# TODO: should we apply time skew? not sure to understand the utility.
|
# TODO: should we apply a time skew? not sure to understand the utility.
|
||||||
|
|
||||||
log.debug('creating clock')
|
log.debug('creating clock')
|
||||||
if self.live_graph or self._analyze_live is not None:
|
if self.live_graph:
|
||||||
self._clock = LiveGraphClock(
|
self._clock = LiveGraphClock(
|
||||||
self.sim_params.sessions,
|
self.sim_params.sessions,
|
||||||
context=self,
|
context=self
|
||||||
callback=self._analyze_live,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._clock = SimpleClock(
|
self._clock = SimpleClock(
|
||||||
@@ -460,126 +393,100 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
|
|
||||||
return self._clock
|
return self._clock
|
||||||
|
|
||||||
def get_generator(self):
|
def _create_generator(self, sim_params):
|
||||||
if self.trading_client is not None:
|
|
||||||
return self.trading_client.transform()
|
|
||||||
|
|
||||||
perf = None
|
|
||||||
if self.perf_tracker is None:
|
if self.perf_tracker is None:
|
||||||
tracker = self.perf_tracker = PerformanceTracker(
|
self.perf_tracker = get_algo_object(
|
||||||
sim_params=self.sim_params,
|
|
||||||
trading_calendar=self.trading_calendar,
|
|
||||||
env=self.trading_environment,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set the dt initially to the period start by forcing it to change.
|
|
||||||
self.on_dt_changed(self.sim_params.start_session)
|
|
||||||
|
|
||||||
# Unpacking the perf_tracker and positions if available
|
|
||||||
perf = get_algo_object(
|
|
||||||
algo_name=self.algo_namespace,
|
algo_name=self.algo_namespace,
|
||||||
key='cumulative_performance',
|
key='perf_tracker'
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self.initialized:
|
|
||||||
self.initialize(*self.initialize_args, **self.initialize_kwargs)
|
|
||||||
self.initialized = True
|
|
||||||
|
|
||||||
# Call the simulation trading algorithm for side-effects:
|
# Call the simulation trading algorithm for side-effects:
|
||||||
# it creates the perf tracker
|
# it creates the perf tracker
|
||||||
# TradingAlgorithm._create_generator(self, self.sim_params)
|
TradingAlgorithm._create_generator(self, sim_params)
|
||||||
if perf is not None:
|
|
||||||
tracker.cumulative_performance = perf
|
|
||||||
|
|
||||||
period = self.perf_tracker.todays_performance
|
|
||||||
period.starting_cash = perf.ending_cash
|
|
||||||
period.starting_exposure = perf.ending_exposure
|
|
||||||
period.starting_value = perf.ending_value
|
|
||||||
period.position_tracker = perf.position_tracker
|
|
||||||
|
|
||||||
self.trading_client = ExchangeAlgorithmExecutor(
|
self.trading_client = ExchangeAlgorithmExecutor(
|
||||||
algo=self,
|
self,
|
||||||
sim_params=self.sim_params,
|
sim_params,
|
||||||
data_portal=self.data_portal,
|
self.data_portal,
|
||||||
clock=self.clock,
|
self.clock,
|
||||||
benchmark_source=self._create_benchmark_source(),
|
self._create_benchmark_source(),
|
||||||
restrictions=self.restrictions,
|
self.restrictions,
|
||||||
universe_func=self._calculate_universe,
|
universe_func=self._calculate_universe
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.trading_client.transform()
|
return self.trading_client.transform()
|
||||||
|
|
||||||
def updated_portfolio(self):
|
def updated_portfolio(self):
|
||||||
|
"""
|
||||||
|
We skip the entire performance tracker business and update the
|
||||||
|
portfolio directly.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ExchangePortfolio
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: build cumulative portfolio
|
||||||
return self.perf_tracker.get_portfolio(False)
|
return self.perf_tracker.get_portfolio(False)
|
||||||
|
|
||||||
def updated_account(self):
|
def updated_account(self):
|
||||||
return self.perf_tracker.get_account(False)
|
return self.perf_tracker.get_account(False)
|
||||||
|
|
||||||
def synchronize_portfolio(self):
|
def _synchronize_portfolio(self, attempt_index=0):
|
||||||
"""
|
try:
|
||||||
Synchronizes the portfolio tracked by the algorithm to refresh
|
for exchange_name in self.exchanges:
|
||||||
its current value.
|
exchange = self.exchanges[exchange_name]
|
||||||
|
|
||||||
This includes updating the last_sale_price of all tracked
|
exchange.synchronize_portfolio()
|
||||||
positions, returning the available cash, and raising error
|
|
||||||
if the data goes out of sync.
|
|
||||||
|
|
||||||
Parameters
|
# Applying the updated last_sales_price to the positions
|
||||||
----------
|
# in the performance tracker. This seems a bit redundant
|
||||||
attempt_index: int
|
# but it will make sense when we have multiple exchange portfolios
|
||||||
|
# feeding into the same performance tracker.
|
||||||
Returns
|
tracker = self.perf_tracker.todays_performance.position_tracker
|
||||||
-------
|
for asset in exchange.portfolio.positions:
|
||||||
float
|
position = exchange.portfolio.positions[asset]
|
||||||
The amount of base currency available for trading.
|
tracker.update_position(
|
||||||
|
asset=asset,
|
||||||
float
|
last_sale_date=position.last_sale_date,
|
||||||
The total value of all tracked positions.
|
last_sale_price=position.last_sale_price
|
||||||
|
)
|
||||||
"""
|
except ExchangeRequestError as e:
|
||||||
check_balances = (not self.simulate_orders)
|
log.warn(
|
||||||
base_currency = None
|
'update portfolio attempt {}: {}'.format(attempt_index, e)
|
||||||
tracker = self.perf_tracker.position_tracker
|
|
||||||
total_cash = 0.0
|
|
||||||
total_positions_value = 0.0
|
|
||||||
|
|
||||||
# Position keys correspond to assets
|
|
||||||
positions = self.portfolio.positions
|
|
||||||
assets = list(positions)
|
|
||||||
exchange_assets = group_assets_by_exchange(assets)
|
|
||||||
for exchange_name in self.exchanges:
|
|
||||||
assets = exchange_assets[exchange_name] \
|
|
||||||
if exchange_name in exchange_assets else []
|
|
||||||
|
|
||||||
exchange_positions = copy.deepcopy(
|
|
||||||
[positions[asset] for asset in assets if asset in positions]
|
|
||||||
)
|
)
|
||||||
|
if attempt_index < self.retry_synchronize_portfolio:
|
||||||
exchange = self.exchanges[exchange_name] # Type: Exchange
|
sleep(self.retry_delay)
|
||||||
|
self._synchronize_portfolio(attempt_index + 1)
|
||||||
if base_currency is None:
|
else:
|
||||||
base_currency = exchange.base_currency
|
raise ExchangePortfolioDataError(
|
||||||
|
data_type='update-portfolio',
|
||||||
cash, positions_value = exchange.sync_positions(
|
attempts=attempt_index,
|
||||||
positions=exchange_positions,
|
error=e
|
||||||
check_balances=check_balances,
|
|
||||||
cash=self.portfolio.cash,
|
|
||||||
)
|
|
||||||
total_cash += cash
|
|
||||||
total_positions_value += positions_value
|
|
||||||
|
|
||||||
# Applying modifications to the original positions
|
|
||||||
for position in exchange_positions:
|
|
||||||
tracker.update_position(
|
|
||||||
asset=position.asset,
|
|
||||||
amount=position.amount,
|
|
||||||
last_sale_date=position.last_sale_date,
|
|
||||||
last_sale_price=position.last_sale_price,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not check_balances:
|
def _check_open_orders(self, attempt_index=0):
|
||||||
total_cash = self.portfolio.cash
|
try:
|
||||||
|
orders = list()
|
||||||
|
for exchange_name in self.exchanges:
|
||||||
|
exchange = self.exchanges[exchange_name]
|
||||||
|
exchange_orders = exchange.check_open_orders()
|
||||||
|
|
||||||
return total_cash, total_positions_value
|
orders += exchange_orders
|
||||||
|
|
||||||
|
return orders
|
||||||
|
except ExchangeRequestError as e:
|
||||||
|
log.warn(
|
||||||
|
'check open orders attempt {}: {}'.format(attempt_index, e)
|
||||||
|
)
|
||||||
|
if attempt_index < self.retry_check_open_orders:
|
||||||
|
sleep(self.retry_delay)
|
||||||
|
return self._check_open_orders(attempt_index + 1)
|
||||||
|
else:
|
||||||
|
raise ExchangePortfolioDataError(
|
||||||
|
data_type='order-status',
|
||||||
|
attempts=attempt_index,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
def add_pnl_stats(self, period_stats):
|
def add_pnl_stats(self, period_stats):
|
||||||
"""
|
"""
|
||||||
@@ -670,37 +577,15 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
if not self.is_running:
|
if not self.is_running:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Resetting the frame stats every day to minimize memory footprint
|
self._synchronize_portfolio()
|
||||||
today = data.current_dt.floor('1D')
|
|
||||||
if self.current_day is not None and today > self.current_day:
|
|
||||||
self.frame_stats = list()
|
|
||||||
|
|
||||||
self.performance_needs_update = False
|
transactions = self._check_open_orders()
|
||||||
new_orders = self.perf_tracker.todays_performance.orders_by_id.keys()
|
if len(transactions) > 0:
|
||||||
if new_orders != self._last_orders:
|
for transaction in transactions:
|
||||||
self.performance_needs_update = True
|
self.perf_tracker.process_transaction(transaction)
|
||||||
|
|
||||||
self._last_orders = new_orders
|
|
||||||
|
|
||||||
if self.performance_needs_update:
|
|
||||||
self.perf_tracker.update_performance()
|
self.perf_tracker.update_performance()
|
||||||
self.performance_needs_update = False
|
|
||||||
|
|
||||||
if self.portfolio_needs_update:
|
|
||||||
cash, positions_value = retry(
|
|
||||||
action=self.synchronize_portfolio,
|
|
||||||
attempts=self.attempts['synchronize_portfolio_attempts'],
|
|
||||||
sleeptime=self.attempts['retry_sleeptime'],
|
|
||||||
retry_exceptions=(ExchangeRequestError,),
|
|
||||||
cleanup=lambda: log.warn('Ordering again.')
|
|
||||||
)
|
|
||||||
self.portfolio_needs_update = False
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'got totals from exchanges, cash: {} positions: {}'.format(
|
|
||||||
cash, positions_value
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if self._handle_data:
|
if self._handle_data:
|
||||||
self._handle_data(self, data)
|
self._handle_data(self, data)
|
||||||
|
|
||||||
@@ -710,114 +595,180 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
self.validate_account_controls()
|
self.validate_account_controls()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._save_stats_csv(self._process_stats(data))
|
# Since the clock runs 24/7, I trying to disable the daily
|
||||||
|
# Performance tracker and keep only minute and cumulative
|
||||||
|
self.perf_tracker.update_performance()
|
||||||
|
|
||||||
|
frame_stats = self.prepare_period_stats(
|
||||||
|
data.current_dt, data.current_dt + timedelta(minutes=1))
|
||||||
|
|
||||||
|
# Saving the last hour in memory
|
||||||
|
self.frame_stats.append(frame_stats)
|
||||||
|
|
||||||
|
self.add_pnl_stats(frame_stats)
|
||||||
|
if self.recorded_vars:
|
||||||
|
self.add_custom_signals_stats(frame_stats)
|
||||||
|
recorded_cols = list(self.recorded_vars.keys())
|
||||||
|
else:
|
||||||
|
recorded_cols = None
|
||||||
|
|
||||||
|
self.add_exposure_stats(frame_stats)
|
||||||
|
|
||||||
|
print_df = pd.DataFrame(list(self.frame_stats))
|
||||||
|
log.info(
|
||||||
|
'statistics for the last {stats_minutes} minutes:\n{stats}'.format(
|
||||||
|
stats_minutes=self.stats_minutes,
|
||||||
|
stats=get_pretty_stats(
|
||||||
|
stats_df=print_df,
|
||||||
|
recorded_cols=recorded_cols,
|
||||||
|
num_rows=self.stats_minutes
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
today = pd.to_datetime('today', utc=True)
|
||||||
|
daily_stats = self.prepare_period_stats(
|
||||||
|
start_dt=today,
|
||||||
|
end_dt=pd.Timestamp.utcnow()
|
||||||
|
)
|
||||||
|
save_algo_object(
|
||||||
|
algo_name=self.algo_namespace,
|
||||||
|
key=today.strftime('%Y-%m-%d'),
|
||||||
|
obj=daily_stats,
|
||||||
|
rel_path='daily_perf'
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warn('unable to calculate performance: {}'.format(e))
|
log.warn('unable to calculate performance: {}'.format(e))
|
||||||
|
|
||||||
save_algo_object(
|
# TODO: pickle does not seem to work in python 3
|
||||||
algo_name=self.algo_namespace,
|
|
||||||
key='cumulative_performance',
|
|
||||||
obj=self.perf_tracker.cumulative_performance,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.current_day = data.current_dt.floor('1D')
|
|
||||||
|
|
||||||
def _process_stats(self, data):
|
|
||||||
today = data.current_dt.floor('1D')
|
|
||||||
|
|
||||||
# Since the clock runs 24/7, I trying to disable the daily
|
|
||||||
# Performance tracker and keep only minute and cumulative
|
|
||||||
self.perf_tracker.update_performance()
|
|
||||||
|
|
||||||
frame_stats = self.prepare_period_stats(
|
|
||||||
data.current_dt, data.current_dt + timedelta(minutes=1)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Saving the last hour in memory
|
|
||||||
self.frame_stats.append(frame_stats)
|
|
||||||
|
|
||||||
self.add_pnl_stats(frame_stats)
|
|
||||||
if self.recorded_vars:
|
|
||||||
self.add_custom_signals_stats(frame_stats)
|
|
||||||
recorded_cols = list(self.recorded_vars.keys())
|
|
||||||
|
|
||||||
else:
|
|
||||||
recorded_cols = None
|
|
||||||
|
|
||||||
self.add_exposure_stats(frame_stats)
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'statistics for the last {stats_minutes} minutes:\n'
|
|
||||||
'{stats}'.format(
|
|
||||||
stats_minutes=self.stats_minutes,
|
|
||||||
stats=get_pretty_stats(
|
|
||||||
stats=self.frame_stats,
|
|
||||||
recorded_cols=recorded_cols,
|
|
||||||
num_rows=self.stats_minutes,
|
|
||||||
)
|
|
||||||
))
|
|
||||||
|
|
||||||
# Saving the daily stats in a format usable for performance
|
|
||||||
# analysis.
|
|
||||||
daily_stats = self.prepare_period_stats(
|
|
||||||
start_dt=today,
|
|
||||||
end_dt=data.current_dt
|
|
||||||
)
|
|
||||||
save_algo_object(
|
|
||||||
algo_name=self.algo_namespace,
|
|
||||||
key=today.strftime('%Y-%m-%d'),
|
|
||||||
obj=daily_stats,
|
|
||||||
rel_path='daily_perf'
|
|
||||||
)
|
|
||||||
|
|
||||||
return recorded_cols
|
|
||||||
|
|
||||||
def _save_stats_csv(self, recorded_cols):
|
|
||||||
# Writing the stats output
|
|
||||||
csv_bytes = None
|
|
||||||
try:
|
try:
|
||||||
csv_bytes = stats_to_algo_folder(
|
save_algo_object(
|
||||||
stats=self.frame_stats,
|
algo_name=self.algo_namespace,
|
||||||
algo_namespace=self.algo_namespace,
|
key='perf_tracker',
|
||||||
recorded_cols=recorded_cols,
|
obj=self.perf_tracker
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warn('unable save stats locally: {}'.format(e))
|
log.warn('unable to save minute perfs to disk: {}'.format(e))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.stats_output is not None:
|
for exchange_name in self.exchanges:
|
||||||
if 's3://' in self.stats_output:
|
exchange = self.exchanges[exchange_name]
|
||||||
stats_to_s3(
|
save_algo_object(
|
||||||
uri=self.stats_output,
|
algo_name=self.algo_namespace,
|
||||||
stats=self.frame_stats,
|
key='portfolio_{}'.format(exchange_name),
|
||||||
algo_namespace=self.algo_namespace,
|
obj=exchange.portfolio
|
||||||
recorded_cols=recorded_cols,
|
)
|
||||||
bytes_to_write=csv_bytes
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
'Only S3 stats output is supported for now.'
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warn('unable save stats externally: {}'.format(e))
|
log.warn('unable to save portfolio to disk: {}'.format(e))
|
||||||
|
|
||||||
|
def _order(self,
|
||||||
|
asset,
|
||||||
|
amount,
|
||||||
|
limit_price=None,
|
||||||
|
stop_price=None,
|
||||||
|
style=None,
|
||||||
|
attempt_index=0):
|
||||||
|
try:
|
||||||
|
exchange = self.exchanges[asset.exchange]
|
||||||
|
return exchange.order(asset, amount, limit_price,
|
||||||
|
stop_price,
|
||||||
|
style)
|
||||||
|
except ExchangeRequestError as e:
|
||||||
|
log.warn(
|
||||||
|
'order attempt {}: {}'.format(attempt_index, e)
|
||||||
|
)
|
||||||
|
if attempt_index < self.retry_order:
|
||||||
|
sleep(self.retry_delay)
|
||||||
|
return self._order(
|
||||||
|
asset, amount, limit_price, stop_price, style,
|
||||||
|
attempt_index + 1)
|
||||||
|
else:
|
||||||
|
raise ExchangeTransactionError(
|
||||||
|
transaction_type='order',
|
||||||
|
attempts=attempt_index,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
|
@api_method
|
||||||
|
@disallowed_in_before_trading_start(OrderInBeforeTradingStart())
|
||||||
|
@expect_types(asset=TradingPair)
|
||||||
|
def order(self,
|
||||||
|
asset,
|
||||||
|
amount,
|
||||||
|
limit_price=None,
|
||||||
|
stop_price=None,
|
||||||
|
style=None):
|
||||||
|
"""
|
||||||
|
We use the exchange specific portfolio to place orders.
|
||||||
|
The cumulative portfolio does not contain open orders but exchange
|
||||||
|
portfolios do.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asset: TradingPair
|
||||||
|
amount: float
|
||||||
|
limit_price: float
|
||||||
|
stop_price: float
|
||||||
|
style: Style
|
||||||
|
order: Order
|
||||||
|
The catalyst order object or None
|
||||||
|
"""
|
||||||
|
amount, style = self._calculate_order(asset, amount,
|
||||||
|
limit_price, stop_price,
|
||||||
|
style)
|
||||||
|
|
||||||
|
order_id = self._order(asset, amount, limit_price, stop_price, style)
|
||||||
|
|
||||||
|
exchange = self.exchanges[asset.exchange]
|
||||||
|
exchange_portfolio = exchange.portfolio
|
||||||
|
if order_id is not None:
|
||||||
|
|
||||||
|
if order_id in exchange_portfolio.open_orders:
|
||||||
|
order = exchange_portfolio.open_orders[order_id]
|
||||||
|
self.perf_tracker.process_order(order)
|
||||||
|
return order
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise OrphanOrderError(
|
||||||
|
order_id=order_id,
|
||||||
|
exchange=exchange.name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.warn('unable to order {} {} on exchange {}'.format(
|
||||||
|
amount, asset.symbol, asset.exchange))
|
||||||
|
return None
|
||||||
|
|
||||||
@api_method
|
@api_method
|
||||||
def batch_market_order(self, share_counts):
|
def batch_market_order(self, share_counts):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _get_open_orders(self, asset=None):
|
def _get_open_orders(self, asset=None, attempt_index=0):
|
||||||
if asset:
|
try:
|
||||||
exchange = self.exchanges[asset.exchange]
|
if asset:
|
||||||
return exchange.get_open_orders(asset)
|
exchange = self.exchanges[asset.exchange]
|
||||||
|
return exchange.get_open_orders(asset)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
open_orders = []
|
open_orders = []
|
||||||
for exchange_name in self.exchanges:
|
for exchange_name in self.exchanges:
|
||||||
exchange = self.exchanges[exchange_name]
|
exchange = self.exchanges[exchange_name]
|
||||||
exchange_orders = exchange.get_open_orders()
|
exchange_orders = exchange.get_open_orders()
|
||||||
open_orders.append(exchange_orders)
|
open_orders.append(exchange_orders)
|
||||||
|
|
||||||
return open_orders
|
return open_orders
|
||||||
|
except ExchangeRequestError as e:
|
||||||
|
log.warn(
|
||||||
|
'open orders attempt {}: {}'.format(attempt_index, e)
|
||||||
|
)
|
||||||
|
if attempt_index < self.retry_get_open_orders:
|
||||||
|
sleep(self.retry_delay)
|
||||||
|
return self._get_open_orders(asset, attempt_index + 1)
|
||||||
|
else:
|
||||||
|
raise ExchangePortfolioDataError(
|
||||||
|
data_type='open-orders',
|
||||||
|
attempts=attempt_index,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
@error_keywords(sid='Keyword argument `sid` is no longer supported for '
|
@error_keywords(sid='Keyword argument `sid` is no longer supported for '
|
||||||
'get_open_orders. Use `asset` instead.')
|
'get_open_orders. Use `asset` instead.')
|
||||||
@@ -839,13 +790,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
If an asset is passed then this will return a list of the open
|
If an asset is passed then this will return a list of the open
|
||||||
orders for this asset.
|
orders for this asset.
|
||||||
"""
|
"""
|
||||||
return retry(
|
return self._get_open_orders(asset)
|
||||||
action=self._get_open_orders,
|
|
||||||
attempts=self.attempts['get_open_orders_attempts'],
|
|
||||||
sleeptime=self.attempts['retry_sleeptime'],
|
|
||||||
retry_exceptions=(ExchangeRequestError,),
|
|
||||||
cleanup=lambda: log.warn('Fetching open orders again.'),
|
|
||||||
args=(asset,))
|
|
||||||
|
|
||||||
@api_method
|
@api_method
|
||||||
def get_order(self, order_id, exchange_name):
|
def get_order(self, order_id, exchange_name):
|
||||||
@@ -865,13 +810,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
The execution price per share of the order
|
The execution price per share of the order
|
||||||
"""
|
"""
|
||||||
exchange = self.exchanges[exchange_name]
|
exchange = self.exchanges[exchange_name]
|
||||||
return retry(
|
return exchange.get_order(order_id)
|
||||||
action=exchange.get_order,
|
|
||||||
attempts=self.attempts['get_order_attempts'],
|
|
||||||
sleeptime=self.attempts['retry_sleeptime'],
|
|
||||||
retry_exceptions=(ExchangeRequestError,),
|
|
||||||
cleanup=lambda: log.warn('Fetching orders again.'),
|
|
||||||
args=(order_id,))
|
|
||||||
|
|
||||||
@api_method
|
@api_method
|
||||||
def cancel_order(self, order_param, exchange_name):
|
def cancel_order(self, order_param, exchange_name):
|
||||||
@@ -888,10 +827,4 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
if isinstance(order_param, zp.Order):
|
if isinstance(order_param, zp.Order):
|
||||||
order_id = order_param.id
|
order_id = order_param.id
|
||||||
|
|
||||||
retry(
|
exchange.cancel_order(order_id)
|
||||||
action=exchange.cancel_order,
|
|
||||||
attempts=self.attempts['cancel_order_attempts'],
|
|
||||||
sleeptime=self.attempts['retry_sleeptime'],
|
|
||||||
retry_exceptions=(ExchangeRequestError,),
|
|
||||||
cleanup=lambda: log.warn('cancelling order again.'),
|
|
||||||
args=(order_id,))
|
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.exchange.utils.factory import find_exchanges
|
|
||||||
|
|
||||||
log = Logger('ExchangeAssetFinder', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeAssetFinder(object):
|
|
||||||
def __init__(self, exchanges):
|
|
||||||
self.exchanges = exchanges
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sids(self):
|
|
||||||
"""
|
|
||||||
This seems to be used to pre-fetch assets.
|
|
||||||
I don't think that we need this for live-trading.
|
|
||||||
Leaving the list empty.
|
|
||||||
"""
|
|
||||||
all_sids = []
|
|
||||||
for exchange_name in self.exchanges:
|
|
||||||
# This is what initializes each exchanges at the beginning
|
|
||||||
# of an algo
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
exchange.init()
|
|
||||||
|
|
||||||
all_sids += [asset.sid for asset in exchange.assets]
|
|
||||||
|
|
||||||
sids = list(set(all_sids))
|
|
||||||
return sids
|
|
||||||
|
|
||||||
def retrieve_asset(self, sid, default_none=False):
|
|
||||||
"""
|
|
||||||
Retrieve the first Asset found for a given sid.
|
|
||||||
"""
|
|
||||||
asset = None
|
|
||||||
for exchange_name in self.exchanges:
|
|
||||||
if asset is not None:
|
|
||||||
break
|
|
||||||
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
assets = [asset for asset in exchange.assets if asset.sid == sid]
|
|
||||||
if assets:
|
|
||||||
asset = assets[0]
|
|
||||||
|
|
||||||
return asset
|
|
||||||
|
|
||||||
def retrieve_all(self, sids, default_none=False):
|
|
||||||
"""
|
|
||||||
Retrieve all assets in `sids`.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
sids : iterable of int
|
|
||||||
Assets to retrieve.
|
|
||||||
default_none : bool
|
|
||||||
If True, return None for failed lookups.
|
|
||||||
If False, raise `SidsNotFound`.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
assets : list[Asset or None]
|
|
||||||
A list of the same length as `sids` containing Assets (or Nones)
|
|
||||||
corresponding to the requested sids.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
SidsNotFound
|
|
||||||
When a requested sid is not found and default_none=False.
|
|
||||||
"""
|
|
||||||
assets = []
|
|
||||||
for exchange_name in self.exchanges:
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
xas = [asset for asset in exchange.assets if asset.sid in sids]
|
|
||||||
assets += xas
|
|
||||||
|
|
||||||
return assets
|
|
||||||
|
|
||||||
def lookup_symbol(self, symbol, exchange, data_frequency=None,
|
|
||||||
as_of_date=None, fuzzy=False):
|
|
||||||
"""Lookup an asset by symbol.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
symbol : str
|
|
||||||
The ticker symbol to resolve.
|
|
||||||
as_of_date : datetime or None
|
|
||||||
Look up the last owner of this symbol as of this datetime.
|
|
||||||
If ``as_of_date`` is None, then this can only resolve the equity
|
|
||||||
if exactly one equity has ever owned the ticker.
|
|
||||||
fuzzy : bool, optional
|
|
||||||
Should fuzzy symbol matching be used? Fuzzy symbol matching
|
|
||||||
attempts to resolve differences in representations for
|
|
||||||
shareclasses. For example, some people may represent the ``A``
|
|
||||||
shareclass of ``BRK`` as ``BRK.A``, where others could write
|
|
||||||
``BRK_A``.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
equity : Asset
|
|
||||||
The equity that held ``symbol`` on the given ``as_of_date``, or the
|
|
||||||
only equity to hold ``symbol`` if ``as_of_date`` is None.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
SymbolNotFound
|
|
||||||
Raised when no equity has ever held the given symbol.
|
|
||||||
MultipleSymbolsFound
|
|
||||||
Raised when no ``as_of_date`` is given and more than one equity
|
|
||||||
has held ``symbol``. This is also raised when ``fuzzy=True`` and
|
|
||||||
there are multiple candidates for the given ``symbol`` on the
|
|
||||||
``as_of_date``.
|
|
||||||
"""
|
|
||||||
log.debug('looking up symbol: {} {}'.format(symbol, exchange.name))
|
|
||||||
|
|
||||||
return exchange.get_asset(symbol, data_frequency)
|
|
||||||
|
|
||||||
def lifetimes(self, dates, include_start_date):
|
|
||||||
"""
|
|
||||||
Compute a DataFrame representing asset lifetimes for the specified date
|
|
||||||
range.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
dates : pd.DatetimeIndex
|
|
||||||
The dates for which to compute lifetimes.
|
|
||||||
include_start_date : bool
|
|
||||||
Whether or not to count the asset as alive on its start_date.
|
|
||||||
|
|
||||||
This is useful in a backtesting context where `lifetimes` is being
|
|
||||||
used to signify "do I have data for this asset as of the morning of
|
|
||||||
this date?" For many financial metrics, (e.g. daily close), data
|
|
||||||
isn't available for an asset until the end of the asset's first
|
|
||||||
day.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
lifetimes : pd.DataFrame
|
|
||||||
A frame of dtype bool with `dates` as index and an Int64Index of
|
|
||||||
assets as columns. The value at `lifetimes.loc[date, asset]` will
|
|
||||||
be True iff `asset` existed on `date`. If `include_start_date` is
|
|
||||||
False, then lifetimes.loc[date, asset] will be false when date ==
|
|
||||||
asset.start_date.
|
|
||||||
|
|
||||||
See Also
|
|
||||||
--------
|
|
||||||
numpy.putmask
|
|
||||||
catalyst.pipeline.engine.SimplePipelineEngine._compute_root_mask
|
|
||||||
"""
|
|
||||||
exchanges = find_exchanges(features=['minuteBundle'])
|
|
||||||
if not exchanges:
|
|
||||||
raise ValueError('exchange with minute bundles not found')
|
|
||||||
|
|
||||||
# TODO: find a way to support multiple exchanges
|
|
||||||
exchange = exchanges[0]
|
|
||||||
# Using a single exchange for now because are not unique for the
|
|
||||||
# same asset in different exchanges. I'd like to avoid binding
|
|
||||||
# pipeline to a single exchange.
|
|
||||||
exchange.init()
|
|
||||||
|
|
||||||
data = []
|
|
||||||
for dt in dates:
|
|
||||||
exists = []
|
|
||||||
|
|
||||||
for asset in exchange.assets:
|
|
||||||
if include_start_date:
|
|
||||||
condition = (asset.start_date <= dt < asset.end_minute)
|
|
||||||
|
|
||||||
else:
|
|
||||||
condition = (asset.start_date < dt < asset.end_minute)
|
|
||||||
|
|
||||||
exists.append(condition)
|
|
||||||
|
|
||||||
data.append(exists)
|
|
||||||
|
|
||||||
sids = [asset.sid for asset in exchange.assets]
|
|
||||||
df = pd.DataFrame(data, index=dates, columns=exchange.assets)
|
|
||||||
|
|
||||||
return df
|
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
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 redo import retry
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.exchange.exchange_errors import ExchangeRequestError
|
|
||||||
from catalyst.finance.blotter import Blotter
|
from catalyst.finance.blotter import Blotter
|
||||||
from catalyst.finance.commission import CommissionModel
|
from catalyst.finance.commission import CommissionModel
|
||||||
from catalyst.finance.order import ORDER_STATUS
|
|
||||||
from catalyst.finance.slippage import SlippageModel
|
from catalyst.finance.slippage import SlippageModel
|
||||||
from catalyst.finance.transaction import create_transaction, Transaction
|
from catalyst.finance.transaction import create_transaction
|
||||||
from catalyst.utils.input_validation import expect_types
|
|
||||||
|
|
||||||
log = Logger('exchange_blotter', level=LOG_LEVEL)
|
log = Logger('exchange_blotter', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
# It seems like we need to accept greater slippage risk in cryptos
|
||||||
|
# Orders won't often close at Equity levels.
|
||||||
|
# TODO: should work with set_commission and set_slippage
|
||||||
|
DEFAULT_SLIPPAGE_SPREAD = 0.0001
|
||||||
|
DEFAULT_MAKER_FEE = 0.0015
|
||||||
|
DEFAULT_TAKER_FEE = 0.0025
|
||||||
|
|
||||||
|
|
||||||
class TradingPairFeeSchedule(CommissionModel):
|
class TradingPairFeeSchedule(CommissionModel):
|
||||||
"""
|
"""
|
||||||
@@ -21,24 +23,23 @@ class TradingPairFeeSchedule(CommissionModel):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
maker : float, optional
|
fee : float, optional
|
||||||
The percentage maker fee.
|
The percentage fee.
|
||||||
|
|
||||||
taker: float, optional
|
|
||||||
The percentage taker fee.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, maker=None, taker=None):
|
def __init__(self,
|
||||||
self.maker = maker
|
maker_fee=DEFAULT_MAKER_FEE,
|
||||||
self.taker = taker
|
taker_fee=DEFAULT_TAKER_FEE):
|
||||||
|
self.maker_fee = maker_fee
|
||||||
|
self.taker_fee = taker_fee
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
'{class_name}(maker={maker}, '
|
'{class_name}(maker_fee={maker_fee}, '
|
||||||
'taker={taker})'.format(
|
'taker_fee={taker_fee})'.format(
|
||||||
class_name=self.__class__.__name__,
|
class_name=self.__class__.__name__,
|
||||||
maker=self.maker,
|
maker_fee=self.maker_fee,
|
||||||
taker=self.taker,
|
taker_fee=self.taker_fee,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,26 +47,16 @@ class TradingPairFeeSchedule(CommissionModel):
|
|||||||
"""
|
"""
|
||||||
Calculate the final fee based on the order parameters.
|
Calculate the final fee based on the order parameters.
|
||||||
|
|
||||||
:param order: Order
|
:param order:
|
||||||
:param transaction: Transaction
|
:param transaction:
|
||||||
|
|
||||||
:return float:
|
:return float:
|
||||||
The total commission.
|
The total commission.
|
||||||
"""
|
"""
|
||||||
cost = abs(transaction.amount) * transaction.price
|
cost = abs(transaction.amount) * transaction.price
|
||||||
|
|
||||||
asset = order.asset
|
# Assuming just the taker fee for now
|
||||||
maker = self.maker if self.maker is not None else asset.maker
|
fee = cost * self.taker_fee
|
||||||
taker = self.taker if self.taker is not None else asset.taker
|
|
||||||
|
|
||||||
multiplier = taker
|
|
||||||
if order.limit is not None:
|
|
||||||
multiplier = maker \
|
|
||||||
if ((order.amount > 0 and order.limit < transaction.price)
|
|
||||||
or (order.amount < 0 and order.limit > transaction.price)) \
|
|
||||||
and order.limit_reached else taker
|
|
||||||
|
|
||||||
fee = cost * multiplier
|
|
||||||
return fee
|
return fee
|
||||||
|
|
||||||
|
|
||||||
@@ -79,7 +70,7 @@ class TradingPairFixedSlippage(SlippageModel):
|
|||||||
spread / 2 will be added to buys and subtracted from sells.
|
spread / 2 will be added to buys and subtracted from sells.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, spread=0.0001):
|
def __init__(self, spread=DEFAULT_SLIPPAGE_SPREAD):
|
||||||
super(TradingPairFixedSlippage, self).__init__()
|
super(TradingPairFixedSlippage, self).__init__()
|
||||||
self.spread = spread
|
self.spread = spread
|
||||||
|
|
||||||
@@ -130,15 +121,6 @@ class TradingPairFixedSlippage(SlippageModel):
|
|||||||
|
|
||||||
class ExchangeBlotter(Blotter):
|
class ExchangeBlotter(Blotter):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.simulate_orders = kwargs.pop('simulate_orders', False)
|
|
||||||
self.attempts = kwargs.pop('attempts', False)
|
|
||||||
|
|
||||||
self.exchanges = kwargs.pop('exchanges', None)
|
|
||||||
if not self.exchanges:
|
|
||||||
raise ValueError(
|
|
||||||
'ExchangeBlotter must have an `exchanges` attribute.'
|
|
||||||
)
|
|
||||||
|
|
||||||
super(ExchangeBlotter, self).__init__(*args, **kwargs)
|
super(ExchangeBlotter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Using the equity models for now
|
# Using the equity models for now
|
||||||
@@ -150,125 +132,3 @@ class ExchangeBlotter(Blotter):
|
|||||||
self.commission_models = {
|
self.commission_models = {
|
||||||
TradingPair: TradingPairFeeSchedule()
|
TradingPair: TradingPairFeeSchedule()
|
||||||
}
|
}
|
||||||
|
|
||||||
def exchange_order(self, asset, amount, style=None):
|
|
||||||
exchange = self.exchanges[asset.exchange]
|
|
||||||
return exchange.order(
|
|
||||||
asset, amount, style
|
|
||||||
)
|
|
||||||
|
|
||||||
@expect_types(asset=TradingPair)
|
|
||||||
def order(self, asset, amount, style, order_id=None):
|
|
||||||
log.debug('ordering {} {}'.format(amount, asset.symbol))
|
|
||||||
if amount == 0:
|
|
||||||
log.warn('skipping 0 amount orders')
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self.simulate_orders:
|
|
||||||
return super(ExchangeBlotter, self).order(
|
|
||||||
asset, amount, style, order_id
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
order = retry(
|
|
||||||
action=self.exchange_order,
|
|
||||||
attempts=self.attempts['order_attempts'],
|
|
||||||
sleeptime=self.attempts['retry_sleeptime'],
|
|
||||||
retry_exceptions=(ExchangeRequestError,),
|
|
||||||
cleanup=lambda: log.warn('Ordering again.'),
|
|
||||||
args=(asset, amount, style),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.open_orders[order.asset].append(order)
|
|
||||||
self.orders[order.id] = order
|
|
||||||
self.new_orders.append(order)
|
|
||||||
|
|
||||||
return order.id
|
|
||||||
|
|
||||||
def check_open_orders(self):
|
|
||||||
"""
|
|
||||||
Loop through the list of open orders in the Portfolio object.
|
|
||||||
For each executed order found, create a transaction and apply to the
|
|
||||||
Portfolio.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
list[Transaction]
|
|
||||||
|
|
||||||
"""
|
|
||||||
for asset in self.open_orders:
|
|
||||||
exchange = self.exchanges[asset.exchange]
|
|
||||||
|
|
||||||
for order in self.open_orders[asset]:
|
|
||||||
log.debug('found open order: {}'.format(order.id))
|
|
||||||
|
|
||||||
new_order, executed_price = exchange.get_order(order.id, asset)
|
|
||||||
log.debug(
|
|
||||||
'got updated order {} {}'.format(
|
|
||||||
new_order, executed_price
|
|
||||||
)
|
|
||||||
)
|
|
||||||
order.status = new_order.status
|
|
||||||
|
|
||||||
if order.status == ORDER_STATUS.FILLED:
|
|
||||||
order.commission = new_order.commission
|
|
||||||
if order.amount != new_order.amount:
|
|
||||||
log.warn(
|
|
||||||
'executed order amount {} differs '
|
|
||||||
'from original'.format(
|
|
||||||
new_order.amount, order.amount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
order.amount = new_order.amount
|
|
||||||
|
|
||||||
transaction = Transaction(
|
|
||||||
asset=order.asset,
|
|
||||||
amount=order.amount,
|
|
||||||
dt=pd.Timestamp.utcnow(),
|
|
||||||
price=executed_price,
|
|
||||||
order_id=order.id,
|
|
||||||
commission=order.commission
|
|
||||||
)
|
|
||||||
yield order, transaction
|
|
||||||
|
|
||||||
elif order.status == ORDER_STATUS.CANCELLED:
|
|
||||||
yield order, None
|
|
||||||
|
|
||||||
else:
|
|
||||||
delta = pd.Timestamp.utcnow() - order.dt
|
|
||||||
log.info(
|
|
||||||
'order {order_id} still open after {delta}'.format(
|
|
||||||
order_id=order.id,
|
|
||||||
delta=delta
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_exchange_transactions(self):
|
|
||||||
closed_orders = []
|
|
||||||
transactions = []
|
|
||||||
commissions = []
|
|
||||||
|
|
||||||
for order, txn in self.check_open_orders():
|
|
||||||
order.dt = txn.dt
|
|
||||||
|
|
||||||
transactions.append(txn)
|
|
||||||
|
|
||||||
if not order.open:
|
|
||||||
closed_orders.append(order)
|
|
||||||
|
|
||||||
return transactions, commissions, closed_orders
|
|
||||||
|
|
||||||
def get_transactions(self, bar_data):
|
|
||||||
if self.simulate_orders:
|
|
||||||
return super(ExchangeBlotter, self).get_transactions(bar_data)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return retry(
|
|
||||||
action=self.get_exchange_transactions,
|
|
||||||
attempts=self.attempts['get_transactions_attempts'],
|
|
||||||
sleeptime=self.attempts['retry_sleeptime'],
|
|
||||||
retry_exceptions=(ExchangeRequestError,),
|
|
||||||
cleanup=lambda: log.warn(
|
|
||||||
'Fetching exchange transactions again.'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from operator import is_not
|
from operator import is_not
|
||||||
@@ -18,18 +18,18 @@ 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, \
|
||||||
|
get_bcolz_chunk, get_month_start_end, \
|
||||||
|
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, DataCorruptionError, PricingDataValueError
|
PricingDataNotLoadedError, DataCorruptionError, PricingDataValueError
|
||||||
from catalyst.exchange.utils.bundle_utils import range_in_bundle, \
|
from catalyst.exchange.exchange_utils import get_exchange_folder, \
|
||||||
get_bcolz_chunk, get_month_start_end, \
|
save_exchange_symbols, mixin_market_params
|
||||||
get_year_start_end, get_df_from_arrays, get_start_dt, get_period_label, \
|
|
||||||
get_delta, get_assets
|
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder, \
|
|
||||||
save_exchange_symbols, mixin_market_params, get_catalyst_symbol
|
|
||||||
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
|
||||||
|
|
||||||
@@ -233,13 +233,11 @@ class ExchangeBundle:
|
|||||||
|
|
||||||
problem = '{name} ({start_dt} to {end_dt}) has empty ' \
|
problem = '{name} ({start_dt} to {end_dt}) has empty ' \
|
||||||
'periods: {dates}'.format(
|
'periods: {dates}'.format(
|
||||||
name=asset.symbol,
|
name=asset.symbol,
|
||||||
start_dt=asset.start_date.strftime(
|
start_dt=asset.start_date.strftime(DATE_TIME_FORMAT),
|
||||||
DATE_TIME_FORMAT),
|
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
||||||
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
dates=[date.strftime(DATE_TIME_FORMAT) for date in dates]
|
||||||
dates=[date.strftime(
|
)
|
||||||
DATE_TIME_FORMAT) for date in dates])
|
|
||||||
|
|
||||||
if empty_rows_behavior == 'warn':
|
if empty_rows_behavior == 'warn':
|
||||||
log.warn(problem)
|
log.warn(problem)
|
||||||
|
|
||||||
@@ -247,7 +245,8 @@ class ExchangeBundle:
|
|||||||
raise EmptyValuesInBundleError(
|
raise EmptyValuesInBundleError(
|
||||||
name=asset.symbol,
|
name=asset.symbol,
|
||||||
end_minute=end_dt,
|
end_minute=end_dt,
|
||||||
dates=dates, )
|
dates=dates
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ohlcv_df.dropna(inplace=True)
|
ohlcv_df.dropna(inplace=True)
|
||||||
@@ -287,12 +286,13 @@ class ExchangeBundle:
|
|||||||
|
|
||||||
problem = '{name} ({start_dt} to {end_dt}) has {threshold} ' \
|
problem = '{name} ({start_dt} to {end_dt}) has {threshold} ' \
|
||||||
'identical close values on: {dates}'.format(
|
'identical close values on: {dates}'.format(
|
||||||
name=asset.symbol,
|
name=asset.symbol,
|
||||||
start_dt=asset.start_date.strftime(DATE_TIME_FORMAT),
|
start_dt=asset.start_date.strftime(DATE_TIME_FORMAT),
|
||||||
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
||||||
threshold=threshold,
|
threshold=threshold,
|
||||||
dates=[pd.to_datetime(date).strftime(DATE_TIME_FORMAT)
|
dates=[pd.to_datetime(date).strftime(DATE_TIME_FORMAT)
|
||||||
for date in dates])
|
for date in dates]
|
||||||
|
)
|
||||||
|
|
||||||
problems.append(problem)
|
problems.append(problem)
|
||||||
|
|
||||||
@@ -462,7 +462,7 @@ class ExchangeBundle:
|
|||||||
(earliest_trade is not None and earliest_trade > start):
|
(earliest_trade is not None and earliest_trade > start):
|
||||||
start = earliest_trade
|
start = earliest_trade
|
||||||
|
|
||||||
if last_entry is not None and (end is None or end > last_entry):
|
if end is None or (last_entry is not None and end > last_entry):
|
||||||
end = last_entry.replace(minute=59, hour=23) \
|
end = last_entry.replace(minute=59, hour=23) \
|
||||||
if data_frequency == 'minute' else last_entry
|
if data_frequency == 'minute' else last_entry
|
||||||
|
|
||||||
@@ -630,8 +630,8 @@ 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:
|
||||||
problems += self.ingest_ctable(
|
problems += self.ingest_ctable(
|
||||||
@@ -668,7 +668,7 @@ class ExchangeBundle:
|
|||||||
|
|
||||||
if self.exchange is None:
|
if self.exchange is None:
|
||||||
# Avoid circular dependencies
|
# Avoid circular dependencies
|
||||||
from catalyst.exchange.utils.factory import get_exchange
|
from catalyst.exchange.factory import get_exchange
|
||||||
self.exchange = get_exchange(self.exchange_name)
|
self.exchange = get_exchange(self.exchange_name)
|
||||||
|
|
||||||
problems = []
|
problems = []
|
||||||
@@ -681,7 +681,6 @@ class ExchangeBundle:
|
|||||||
last_traded=np.object_,
|
last_traded=np.object_,
|
||||||
open=np.float64,
|
open=np.float64,
|
||||||
high=np.float64,
|
high=np.float64,
|
||||||
low=np.float64,
|
|
||||||
close=np.float64,
|
close=np.float64,
|
||||||
volume=np.float64
|
volume=np.float64
|
||||||
),
|
),
|
||||||
@@ -731,7 +730,7 @@ class ExchangeBundle:
|
|||||||
if data_frequency == 'minute' else asset_def['end_minute']
|
if data_frequency == 'minute' else asset_def['end_minute']
|
||||||
|
|
||||||
else:
|
else:
|
||||||
params['symbol'] = get_catalyst_symbol(market)
|
params['symbol'] = self.exchange.get_catalyst_symbol(market)
|
||||||
|
|
||||||
params['end_daily'] = end_dt \
|
params['end_daily'] = end_dt \
|
||||||
if data_frequency == 'daily' else 'N/A'
|
if data_frequency == 'daily' else 'N/A'
|
||||||
@@ -756,10 +755,9 @@ class ExchangeBundle:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for symbol in assets:
|
for symbol in assets:
|
||||||
# here the symbol is the market['id']
|
|
||||||
asset = assets[symbol]
|
asset = assets[symbol]
|
||||||
ohlcv_df = df.loc[
|
ohlcv_df = df.loc[
|
||||||
(df.index.get_level_values(0) == asset.symbol)
|
(df.index.get_level_values(0) == symbol)
|
||||||
] # type: pd.DataFrame
|
] # type: pd.DataFrame
|
||||||
ohlcv_df.index = ohlcv_df.index.droplevel(0)
|
ohlcv_df.index = ohlcv_df.index.droplevel(0)
|
||||||
|
|
||||||
@@ -807,7 +805,7 @@ class ExchangeBundle:
|
|||||||
else:
|
else:
|
||||||
if self.exchange is None:
|
if self.exchange is None:
|
||||||
# Avoid circular dependencies
|
# Avoid circular dependencies
|
||||||
from catalyst.exchange.utils.factory import get_exchange
|
from catalyst.exchange.factory import get_exchange
|
||||||
self.exchange = get_exchange(self.exchange_name)
|
self.exchange = get_exchange(self.exchange_name)
|
||||||
|
|
||||||
assets = get_assets(
|
assets = get_assets(
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import abc
|
import abc
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
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 redo import retry
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL, AUTO_INGEST
|
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 (
|
||||||
ExchangeRequestError,
|
ExchangeRequestError,
|
||||||
|
ExchangeBarDataError,
|
||||||
PricingDataNotLoadedError)
|
PricingDataNotLoadedError)
|
||||||
from catalyst.exchange.utils.exchange_utils import get_frequency, \
|
from catalyst.exchange.exchange_utils import get_frequency, resample_history_df
|
||||||
resample_history_df, group_assets_by_exchange
|
|
||||||
|
|
||||||
log = Logger('DataPortalExchange', level=LOG_LEVEL)
|
log = Logger('DataPortalExchange', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class DataPortalExchangeBase(DataPortal):
|
class DataPortalExchangeBase(DataPortal):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.attempts = dict(
|
|
||||||
get_spot_value_attempts=5,
|
# TODO: put somewhere accessible by each algo
|
||||||
get_history_window_attempts=5,
|
self.retry_get_history_window = 5
|
||||||
retry_sleeptime=5,
|
self.retry_get_spot_value = 5
|
||||||
)
|
self.retry_delay = 5
|
||||||
|
|
||||||
super(DataPortalExchangeBase, self).__init__(*args, **kwargs)
|
super(DataPortalExchangeBase, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -35,14 +35,39 @@ class DataPortalExchangeBase(DataPortal):
|
|||||||
frequency,
|
frequency,
|
||||||
field,
|
field,
|
||||||
data_frequency,
|
data_frequency,
|
||||||
ffill=True):
|
ffill=True,
|
||||||
exchange_assets = group_assets_by_exchange(assets)
|
attempt_index=0):
|
||||||
if len(exchange_assets) > 1:
|
try:
|
||||||
df_list = []
|
exchange_assets = dict()
|
||||||
for exchange_name in exchange_assets:
|
for asset in assets:
|
||||||
assets = exchange_assets[exchange_name]
|
if asset.exchange not in exchange_assets:
|
||||||
|
exchange_assets[asset.exchange] = list()
|
||||||
|
|
||||||
df_exchange = self.get_exchange_history_window(
|
exchange_assets[asset.exchange].append(asset)
|
||||||
|
|
||||||
|
if len(exchange_assets) > 1:
|
||||||
|
df_list = []
|
||||||
|
for exchange_name in exchange_assets:
|
||||||
|
assets = exchange_assets[exchange_name]
|
||||||
|
|
||||||
|
df_exchange = self.get_exchange_history_window(
|
||||||
|
exchange_name,
|
||||||
|
assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency,
|
||||||
|
ffill)
|
||||||
|
|
||||||
|
df_list.append(df_exchange)
|
||||||
|
|
||||||
|
# Merging the values values of each exchange
|
||||||
|
return pd.concat(df_list)
|
||||||
|
|
||||||
|
else:
|
||||||
|
exchange_name = list(exchange_assets.keys())[0]
|
||||||
|
return self.get_exchange_history_window(
|
||||||
exchange_name,
|
exchange_name,
|
||||||
assets,
|
assets,
|
||||||
end_dt,
|
end_dt,
|
||||||
@@ -52,22 +77,26 @@ class DataPortalExchangeBase(DataPortal):
|
|||||||
data_frequency,
|
data_frequency,
|
||||||
ffill)
|
ffill)
|
||||||
|
|
||||||
df_list.append(df_exchange)
|
except ExchangeRequestError as e:
|
||||||
|
log.warn(
|
||||||
# Merging the values values of each exchange
|
'get history attempt {}: {}'.format(attempt_index, e)
|
||||||
return pd.concat(df_list)
|
)
|
||||||
|
if attempt_index < self.retry_get_history_window:
|
||||||
else:
|
sleep(self.retry_delay)
|
||||||
exchange_name = list(exchange_assets.keys())[0]
|
return self._get_history_window(assets,
|
||||||
return self.get_exchange_history_window(
|
end_dt,
|
||||||
exchange_name,
|
bar_count,
|
||||||
assets,
|
frequency,
|
||||||
end_dt,
|
field,
|
||||||
bar_count,
|
data_frequency,
|
||||||
frequency,
|
ffill,
|
||||||
field,
|
attempt_index + 1)
|
||||||
data_frequency,
|
else:
|
||||||
ffill)
|
raise ExchangeBarDataError(
|
||||||
|
data_type='history',
|
||||||
|
attempts=attempt_index,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
def get_history_window(self,
|
def get_history_window(self,
|
||||||
assets,
|
assets,
|
||||||
@@ -81,19 +110,13 @@ class DataPortalExchangeBase(DataPortal):
|
|||||||
if field == 'price':
|
if field == 'price':
|
||||||
field = 'close'
|
field = 'close'
|
||||||
|
|
||||||
return retry(
|
return self._get_history_window(assets,
|
||||||
action=self._get_history_window,
|
end_dt,
|
||||||
attempts=self.attempts['get_history_window_attempts'],
|
bar_count,
|
||||||
sleeptime=self.attempts['retry_sleeptime'],
|
frequency,
|
||||||
retry_exceptions=(ExchangeRequestError,),
|
field,
|
||||||
cleanup=lambda: log.warn('fetching history again.'),
|
data_frequency,
|
||||||
args=(assets,
|
ffill)
|
||||||
end_dt,
|
|
||||||
bar_count,
|
|
||||||
frequency,
|
|
||||||
field,
|
|
||||||
data_frequency,
|
|
||||||
ffill))
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_exchange_history_window(self,
|
def get_exchange_history_window(self,
|
||||||
@@ -107,58 +130,69 @@ class DataPortalExchangeBase(DataPortal):
|
|||||||
ffill=True):
|
ffill=True):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _get_spot_value(self, assets, field, dt, data_frequency):
|
def _get_spot_value(self, assets, field, dt, data_frequency,
|
||||||
if isinstance(assets, TradingPair):
|
attempt_index=0):
|
||||||
spot_values = self.get_exchange_spot_value(
|
try:
|
||||||
assets.exchange, [assets], field, dt, data_frequency)
|
if isinstance(assets, TradingPair):
|
||||||
|
spot_values = self.get_exchange_spot_value(
|
||||||
|
assets.exchange, [assets], field, dt, data_frequency)
|
||||||
|
|
||||||
if not spot_values:
|
if not spot_values:
|
||||||
return np.nan
|
return np.nan
|
||||||
|
|
||||||
return spot_values[0]
|
return spot_values[0]
|
||||||
|
|
||||||
else:
|
|
||||||
exchange_assets = dict()
|
|
||||||
for asset in assets:
|
|
||||||
if asset.exchange not in exchange_assets:
|
|
||||||
exchange_assets[asset.exchange] = list()
|
|
||||||
|
|
||||||
exchange_assets[asset.exchange].append(asset)
|
|
||||||
|
|
||||||
if len(list(exchange_assets.keys())) == 1:
|
|
||||||
exchange_name = list(exchange_assets.keys())[0]
|
|
||||||
return self.get_exchange_spot_value(
|
|
||||||
exchange_name, assets, field, dt, data_frequency)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
spot_values = []
|
exchange_assets = dict()
|
||||||
for exchange_name in exchange_assets:
|
for asset in assets:
|
||||||
assets = exchange_assets[exchange_name]
|
if asset.exchange not in exchange_assets:
|
||||||
exchange_spot_values = self.get_exchange_spot_value(
|
exchange_assets[asset.exchange] = list()
|
||||||
exchange_name,
|
|
||||||
assets,
|
|
||||||
field,
|
|
||||||
dt,
|
|
||||||
data_frequency
|
|
||||||
)
|
|
||||||
if len(assets) == 1:
|
|
||||||
spot_values.append(exchange_spot_values)
|
|
||||||
else:
|
|
||||||
spot_values += exchange_spot_values
|
|
||||||
|
|
||||||
return spot_values
|
exchange_assets[asset.exchange].append(asset)
|
||||||
|
|
||||||
|
if len(list(exchange_assets.keys())) == 1:
|
||||||
|
exchange_name = list(exchange_assets.keys())[0]
|
||||||
|
return self.get_exchange_spot_value(
|
||||||
|
exchange_name, assets, field, dt, data_frequency)
|
||||||
|
|
||||||
|
else:
|
||||||
|
spot_values = []
|
||||||
|
for exchange_name in exchange_assets:
|
||||||
|
assets = exchange_assets[exchange_name]
|
||||||
|
exchange_spot_values = self.get_exchange_spot_value(
|
||||||
|
exchange_name,
|
||||||
|
assets,
|
||||||
|
field,
|
||||||
|
dt,
|
||||||
|
data_frequency
|
||||||
|
)
|
||||||
|
if len(assets) == 1:
|
||||||
|
spot_values.append(exchange_spot_values)
|
||||||
|
else:
|
||||||
|
spot_values += exchange_spot_values
|
||||||
|
|
||||||
|
return spot_values
|
||||||
|
|
||||||
|
except ExchangeRequestError as e:
|
||||||
|
log.warn(
|
||||||
|
'get spot value attempt {}: {}'.format(attempt_index, e)
|
||||||
|
)
|
||||||
|
if attempt_index < self.retry_get_spot_value:
|
||||||
|
sleep(self.retry_delay)
|
||||||
|
return self._get_spot_value(assets, field, dt, data_frequency,
|
||||||
|
attempt_index + 1)
|
||||||
|
else:
|
||||||
|
raise ExchangeBarDataError(
|
||||||
|
data_type='spot',
|
||||||
|
attempts=attempt_index,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
def get_spot_value(self, assets, field, dt, data_frequency):
|
def get_spot_value(self, assets, field, dt, data_frequency):
|
||||||
if field == 'price':
|
if field == 'price':
|
||||||
field = 'close'
|
field = 'close'
|
||||||
|
|
||||||
return retry(
|
return self._get_spot_value(assets, field, dt, data_frequency)
|
||||||
action=self._get_spot_value,
|
|
||||||
attempts=self.attempts['get_spot_value_attempts'],
|
|
||||||
sleeptime=self.attempts['retry_sleeptime'],
|
|
||||||
retry_exceptions=(ExchangeRequestError,),
|
|
||||||
cleanup=lambda: log.warn('fetching spot value again.'),
|
|
||||||
args=(assets, field, dt, data_frequency))
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_exchange_spot_value(self, exchange_name, assets, field, dt,
|
def get_exchange_spot_value(self, exchange_name, assets, field, dt,
|
||||||
@@ -208,7 +242,6 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
exchange = self.exchanges[exchange_name]
|
exchange = self.exchanges[exchange_name]
|
||||||
|
|
||||||
df = exchange.get_history_window(
|
df = exchange.get_history_window(
|
||||||
assets,
|
assets,
|
||||||
end_dt,
|
end_dt,
|
||||||
@@ -216,7 +249,7 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
|
|||||||
frequency,
|
frequency,
|
||||||
field,
|
field,
|
||||||
data_frequency,
|
data_frequency,
|
||||||
False)
|
ffill)
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def get_exchange_spot_value(self, exchange_name, assets, field, dt,
|
def get_exchange_spot_value(self, exchange_name, assets, field, dt,
|
||||||
@@ -310,7 +343,7 @@ 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,
|
trailing_bar_count=trailing_bar_count
|
||||||
)
|
)
|
||||||
|
|
||||||
df = resample_history_df(pd.DataFrame(series), freq, field)
|
df = resample_history_df(pd.DataFrame(series), freq, field)
|
||||||
|
|||||||
@@ -100,12 +100,6 @@ class InvalidHistoryFrequencyError(ZiplineError):
|
|||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class InvalidHistoryTimeframeError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'CCXT timeframe {timeframe} not supported by the exchange.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class MismatchingFrequencyError(ZiplineError):
|
class MismatchingFrequencyError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Bar aggregate frequency {frequency} not compatible with '
|
'Bar aggregate frequency {frequency} not compatible with '
|
||||||
@@ -149,8 +143,7 @@ class OrphanOrderError(ZiplineError):
|
|||||||
|
|
||||||
class OrphanOrderReverseError(ZiplineError):
|
class OrphanOrderReverseError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Order {order_id} tracked by algorithm, but not found in exchange '
|
'Order {order_id} tracked by algorithm, but not found in exchange {exchange}.'
|
||||||
'{exchange}.'
|
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -168,8 +161,8 @@ class SidHashError(ZiplineError):
|
|||||||
|
|
||||||
class BaseCurrencyNotFoundError(ZiplineError):
|
class BaseCurrencyNotFoundError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Algorithm base currency {base_currency} not found in account '
|
'Algorithm base currency {base_currency} not found in exchange '
|
||||||
'balances on {exchange}: {balances}'
|
'{exchange}.'
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -213,9 +206,8 @@ class EmptyValuesInBundleError(ZiplineError):
|
|||||||
|
|
||||||
class PricingDataBeforeTradingError(ZiplineError):
|
class PricingDataBeforeTradingError(ZiplineError):
|
||||||
msg = ('Pricing data for trading pairs {symbols} on exchange {exchange} '
|
msg = ('Pricing data for trading pairs {symbols} on exchange {exchange} '
|
||||||
'starts on {first_trading_day}, but you are either trying to trade '
|
'starts on {first_trading_day}, but you are either trying to trade or '
|
||||||
'or retrieve pricing data on {dt}. Adjust your dates accordingly.'
|
'retrieve pricing data on {dt}. Adjust your dates accordingly.').strip()
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class PricingDataNotLoadedError(ZiplineError):
|
class PricingDataNotLoadedError(ZiplineError):
|
||||||
@@ -225,93 +217,30 @@ class PricingDataNotLoadedError(ZiplineError):
|
|||||||
'{data_frequency} -i {symbol_list}`. See catalyst documentation '
|
'{data_frequency} -i {symbol_list}`. See catalyst documentation '
|
||||||
'for details.').strip()
|
'for details.').strip()
|
||||||
|
|
||||||
|
|
||||||
class PricingDataValueError(ZiplineError):
|
class PricingDataValueError(ZiplineError):
|
||||||
msg = ('Unable to retrieve pricing data for {exchange} {symbol} '
|
msg = ('Unable to retrieve pricing data for {exchange} {symbol} '
|
||||||
'[{start_dt} - {end_dt}]: {error}').strip()
|
'[{start_dt} - {end_dt}]: {error}').strip()
|
||||||
|
|
||||||
|
|
||||||
class DataCorruptionError(ZiplineError):
|
class DataCorruptionError(ZiplineError):
|
||||||
msg = (
|
msg = ('Unable to validate data for {exchange} {symbols} in date range '
|
||||||
'Unable to validate data for {exchange} {symbols} in date range '
|
'[{start_dt} - {end_dt}]. The data is either corrupted or '
|
||||||
'[{start_dt} - {end_dt}]. The data is either corrupted or '
|
'unavailable. Please try deleting this bundle:'
|
||||||
'unavailable. Please try deleting this bundle:'
|
'\n`catalyst clean-exchange -x {exchange}\n'
|
||||||
'\n`catalyst clean-exchange -x {exchange}\n'
|
'Then, ingest the data again. Please contact the Catalyst team if '
|
||||||
'Then, ingest the data again. Please contact the Catalyst team if '
|
'the issue persists.').strip()
|
||||||
'the issue persists.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class ApiCandlesError(ZiplineError):
|
class ApiCandlesError(ZiplineError):
|
||||||
msg = (
|
msg = ('Unable to fetch candles from the remote API: {error}.').strip()
|
||||||
'Unable to fetch candles from the remote API: {error}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class NoDataAvailableOnExchange(ZiplineError):
|
class NoDataAvailableOnExchange(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Requested data for trading pair {symbol} is not available on '
|
'Requested data for trading pair {symbol} is not available on exchange {exchange} '
|
||||||
'exchange {exchange} '
|
|
||||||
'in `{data_frequency}` frequency at this time. '
|
'in `{data_frequency}` frequency at this time. '
|
||||||
'Check `http://enigma.co/catalyst/status` for market coverage.'
|
'Check `http://enigma.co/catalyst/status` for market coverage.').strip()
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class NoValueForField(ZiplineError):
|
class NoValueForField(ZiplineError):
|
||||||
msg = (
|
msg = ('Value not found for field: {field}.').strip()
|
||||||
'Value not found for field: {field}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class OrderTypeNotSupported(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Order type `{order_type}` not currency supported by Catalyst. '
|
|
||||||
'Please use `limit` or `market` orders only.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class NotEnoughCapitalError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Not enough capital on exchange {exchange} for trading. Each '
|
|
||||||
'exchange should contain at least as much {base_currency} '
|
|
||||||
'as the specified `capital_base`. The current balance {balance} is '
|
|
||||||
'lower than the `capital_base`: {capital_base}'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class NotEnoughCashError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Total {currency} amount on {exchange} is lower than the cash '
|
|
||||||
'reserved for this algo: {free} < {cash}. While trades can be made on '
|
|
||||||
'the exchange accounts outside of the algo, exchange must have enough '
|
|
||||||
'free {currency} to cover the algo cash.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class LastCandleTooEarlyError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'The trade date of the last candle {last_traded} is before the '
|
|
||||||
'specified end date minus one candle {end_dt}. Please verify how '
|
|
||||||
'{exchange} calculates the start date of OHLCV candles.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class TickerNotFoundError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Unable to fetch ticker for {symbol} on {exchange}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class BalanceNotFoundError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'{currency} not found in account balance on {exchange}: {balances}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class BalanceTooLowError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Balance for {currency} on {exchange} too low: {free} < {amount}. '
|
|
||||||
'Positions have likely been sold outside of this algorithm. Please '
|
|
||||||
'add positions to hold a free amount greater than {amount}, or clean '
|
|
||||||
'the state of this algo and restart.'
|
|
||||||
).strip()
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from catalyst.finance.execution import LimitOrder, StopOrder, StopLimitOrder
|
from catalyst.finance.execution import LimitOrder, StopOrder, StopLimitOrder, MarketOrder
|
||||||
|
|
||||||
|
|
||||||
class ExchangeLimitOrder(LimitOrder):
|
class ExchangeLimitOrder(LimitOrder):
|
||||||
|
|||||||
@@ -40,13 +40,7 @@ class ExchangePortfolio(Portfolio):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
log.debug('creating order {}'.format(order.id))
|
log.debug('creating order {}'.format(order.id))
|
||||||
|
self.open_orders[order.id] = order
|
||||||
open_orders = self.open_orders[order.asset] \
|
|
||||||
if order.asset is self.open_orders else []
|
|
||||||
|
|
||||||
open_orders.append(order)
|
|
||||||
|
|
||||||
self.open_orders[order.asset] = open_orders
|
|
||||||
|
|
||||||
order_position = self.positions[order.asset] \
|
order_position = self.positions[order.asset] \
|
||||||
if order.asset in self.positions else None
|
if order.asset in self.positions else None
|
||||||
@@ -58,17 +52,6 @@ class ExchangePortfolio(Portfolio):
|
|||||||
order_position.amount += order.amount
|
order_position.amount += order.amount
|
||||||
log.debug('open order added to portfolio')
|
log.debug('open order added to portfolio')
|
||||||
|
|
||||||
def _remove_open_order(self, order):
|
|
||||||
try:
|
|
||||||
open_orders = self.open_orders[order.asset]
|
|
||||||
if order in open_orders:
|
|
||||||
open_orders.remove(order)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
raise ValueError(
|
|
||||||
'unable to clear order not found in open order list.'
|
|
||||||
)
|
|
||||||
|
|
||||||
def execute_order(self, order, transaction):
|
def execute_order(self, order, transaction):
|
||||||
"""
|
"""
|
||||||
Update the open orders and positions to apply an executed order.
|
Update the open orders and positions to apply an executed order.
|
||||||
@@ -83,15 +66,14 @@ class ExchangePortfolio(Portfolio):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
log.debug('executing order {}'.format(order.id))
|
log.debug('executing order {}'.format(order.id))
|
||||||
self._remove_open_order(order)
|
del self.open_orders[order.id]
|
||||||
|
|
||||||
order_position = self.positions[order.asset] \
|
order_position = self.positions[order.asset] \
|
||||||
if order.asset in self.positions else None
|
if order.asset in self.positions else None
|
||||||
|
|
||||||
if order_position is None:
|
if order_position is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Trying to execute order for a position not held:'
|
'Trying to execute order for a position not held: %s' % order.id
|
||||||
' {}'.format(order.id)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.capital_used += order.amount * transaction.price
|
self.capital_used += order.amount * transaction.price
|
||||||
@@ -117,7 +99,7 @@ class ExchangePortfolio(Portfolio):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
log.info('removing cancelled order {}'.format(order.id))
|
log.info('removing cancelled order {}'.format(order.id))
|
||||||
self._remove_open_order(order)
|
del self.open_orders[order.id]
|
||||||
|
|
||||||
order_position = self.positions[order.asset] \
|
order_position = self.positions[order.asset] \
|
||||||
if order.asset in self.positions else None
|
if order.asset in self.positions else None
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
# 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.
|
|
||||||
from logbook import Logger
|
|
||||||
from numpy import (
|
|
||||||
iinfo,
|
|
||||||
uint32,
|
|
||||||
)
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.data.us_equity_pricing import BcolzDailyBarReader
|
|
||||||
from catalyst.errors import NoFurtherDataError
|
|
||||||
from catalyst.exchange.utils.factory import get_exchange
|
|
||||||
from catalyst.lib.adjusted_array import AdjustedArray
|
|
||||||
from catalyst.pipeline.data import DataSet, Column
|
|
||||||
from catalyst.pipeline.loaders.base import PipelineLoader
|
|
||||||
from catalyst.utils.calendars import get_calendar
|
|
||||||
from catalyst.utils.numpy_utils import float64_dtype
|
|
||||||
|
|
||||||
UINT32_MAX = iinfo(uint32).max
|
|
||||||
|
|
||||||
log = Logger('ExchangePriceLoader', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class TradingPairPricing(DataSet):
|
|
||||||
"""
|
|
||||||
Dataset representing daily trading prices and volumes.
|
|
||||||
"""
|
|
||||||
open = Column(float64_dtype)
|
|
||||||
high = Column(float64_dtype)
|
|
||||||
low = Column(float64_dtype)
|
|
||||||
close = Column(float64_dtype)
|
|
||||||
volume = Column(float64_dtype)
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangePricingLoader(PipelineLoader):
|
|
||||||
"""
|
|
||||||
PipelineLoader for Crypto Pricing data
|
|
||||||
|
|
||||||
Delegates loading of baselines and adjustments.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, data_frequency):
|
|
||||||
|
|
||||||
cal = get_calendar('OPEN')
|
|
||||||
|
|
||||||
if data_frequency == 'daily':
|
|
||||||
reader = None
|
|
||||||
all_sessions = cal.all_sessions
|
|
||||||
|
|
||||||
elif data_frequency == 'minute':
|
|
||||||
reader = None
|
|
||||||
all_sessions = cal.all_minutes
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
'Invalid data frequency: {}'.format(data_frequency)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.data_frequency = data_frequency
|
|
||||||
self.raw_price_loader = reader
|
|
||||||
self._columns = TradingPairPricing.columns
|
|
||||||
self._all_sessions = all_sessions
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_files(cls, pricing_path):
|
|
||||||
"""
|
|
||||||
Create a loader from a bcolz equity pricing dir and a SQLite
|
|
||||||
adjustments path.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
pricing_path : str
|
|
||||||
Path to a bcolz directory written by a BcolzDailyBarWriter.
|
|
||||||
"""
|
|
||||||
return cls(
|
|
||||||
BcolzDailyBarReader(pricing_path),
|
|
||||||
)
|
|
||||||
|
|
||||||
def load_adjusted_array(self, columns, dates, assets, mask):
|
|
||||||
# load_adjusted_array is called with dates on which the user's algo
|
|
||||||
# will be shown data, which means we need to return the data that would
|
|
||||||
# be known at the start of each date. We assume that the latest data
|
|
||||||
# known on day N is the data from day (N - 1), so we shift all query
|
|
||||||
# dates back by a day.
|
|
||||||
start_date, end_date = _shift_dates(
|
|
||||||
self._all_sessions, dates[0], dates[-1], shift=1,
|
|
||||||
)
|
|
||||||
colnames = [c.name for c in columns]
|
|
||||||
|
|
||||||
if len(assets) == 0:
|
|
||||||
raise ValueError(
|
|
||||||
'Pipeline cannot load data with eligible assets.'
|
|
||||||
)
|
|
||||||
|
|
||||||
exchange_names = []
|
|
||||||
for asset in assets:
|
|
||||||
if asset.exchange not in exchange_names:
|
|
||||||
exchange_names.append(asset.exchange)
|
|
||||||
|
|
||||||
exchange = get_exchange(exchange_names[0])
|
|
||||||
reader = exchange.bundle.get_reader(self.data_frequency)
|
|
||||||
|
|
||||||
raw_arrays = reader.load_raw_arrays(
|
|
||||||
colnames,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
assets,
|
|
||||||
)
|
|
||||||
|
|
||||||
out = {}
|
|
||||||
for c, c_raw in zip(columns, raw_arrays):
|
|
||||||
out[c] = AdjustedArray(
|
|
||||||
c_raw.astype(c.dtype),
|
|
||||||
mask,
|
|
||||||
{},
|
|
||||||
c.missing_value,
|
|
||||||
)
|
|
||||||
return out
|
|
||||||
|
|
||||||
@property
|
|
||||||
def columns(self):
|
|
||||||
return self._columns
|
|
||||||
|
|
||||||
|
|
||||||
def _shift_dates(dates, start_date, end_date, shift):
|
|
||||||
try:
|
|
||||||
start = dates.get_loc(start_date)
|
|
||||||
except KeyError:
|
|
||||||
if start_date < dates[0]:
|
|
||||||
raise NoFurtherDataError(
|
|
||||||
msg=(
|
|
||||||
"Pipeline Query requested data starting on {query_start}, "
|
|
||||||
"but first known date is {calendar_start}"
|
|
||||||
).format(
|
|
||||||
query_start=str(start_date),
|
|
||||||
calendar_start=str(dates[0]),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError("Query start %s not in calendar" % start_date)
|
|
||||||
|
|
||||||
# Make sure that shifting doesn't push us out of the calendar.
|
|
||||||
if start < shift:
|
|
||||||
raise NoFurtherDataError(
|
|
||||||
msg=(
|
|
||||||
"Pipeline Query requested data from {shift}"
|
|
||||||
" days before {query_start}, but first known date is only "
|
|
||||||
"{start} days earlier."
|
|
||||||
).format(shift=shift, query_start=start_date, start=start),
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
end = dates.get_loc(end_date)
|
|
||||||
except KeyError:
|
|
||||||
if end_date > dates[-1]:
|
|
||||||
raise NoFurtherDataError(
|
|
||||||
msg=(
|
|
||||||
"Pipeline Query requesting data up to {query_end}, "
|
|
||||||
"but last known date is {calendar_end}"
|
|
||||||
).format(
|
|
||||||
query_end=end_date,
|
|
||||||
calendar_end=dates[-1],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError("Query end %s not in calendar" % end_date)
|
|
||||||
return dates[start - shift], dates[end - shift]
|
|
||||||
@@ -14,8 +14,6 @@ from six.moves.urllib import request
|
|||||||
from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
|
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.exchange.utils.serialization_utils import ExchangeJSONEncoder, \
|
|
||||||
ExchangeJSONDecoder
|
|
||||||
from catalyst.utils.paths import data_root, ensure_directory, \
|
from catalyst.utils.paths import data_root, ensure_directory, \
|
||||||
last_modified_time
|
last_modified_time
|
||||||
|
|
||||||
@@ -64,13 +62,6 @@ def get_exchange_folder(exchange_name, environ=None):
|
|||||||
return exchange_folder
|
return exchange_folder
|
||||||
|
|
||||||
|
|
||||||
def is_blacklist(exchange_name, environ=None):
|
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
|
||||||
filename = os.path.join(exchange_folder, 'blacklist.txt')
|
|
||||||
|
|
||||||
return os.path.exists(filename)
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_symbols_filename(exchange_name, is_local=False, 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.
|
||||||
@@ -110,6 +101,20 @@ def download_exchange_symbols(exchange_name, environ=None):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def symbols_parser(asset_def):
|
||||||
|
for key, value in asset_def.items():
|
||||||
|
match = isinstance(value, string_types) \
|
||||||
|
and re.search(r'(\d{4}-\d{2}-\d{2})', value)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
try:
|
||||||
|
asset_def[key] = pd.to_datetime(value, utc=True)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return asset_def
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_symbols(exchange_name, is_local=False, 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.
|
||||||
@@ -135,10 +140,10 @@ def get_exchange_symbols(exchange_name, is_local=False, environ=None):
|
|||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
with open(filename) as data_file:
|
with open(filename) as data_file:
|
||||||
try:
|
try:
|
||||||
data = json.load(data_file, cls=ExchangeJSONDecoder)
|
data = json.load(data_file, object_hook=symbols_parser)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
return dict()
|
return dict()
|
||||||
else:
|
else:
|
||||||
raise ExchangeSymbolsNotFound(
|
raise ExchangeSymbolsNotFound(
|
||||||
@@ -261,7 +266,7 @@ def get_algo_folder(algo_name, environ=None):
|
|||||||
return algo_folder
|
return algo_folder
|
||||||
|
|
||||||
|
|
||||||
def get_algo_object(algo_name, key, environ=None, rel_path=None, how='pickle'):
|
def get_algo_object(algo_name, key, environ=None, rel_path=None):
|
||||||
"""
|
"""
|
||||||
The de-serialized object of the algo name and key.
|
The de-serialized object of the algo name and key.
|
||||||
|
|
||||||
@@ -285,25 +290,19 @@ def get_algo_object(algo_name, key, environ=None, rel_path=None, how='pickle'):
|
|||||||
if rel_path is not None:
|
if rel_path is not None:
|
||||||
folder = os.path.join(folder, rel_path)
|
folder = os.path.join(folder, rel_path)
|
||||||
|
|
||||||
name = '{}.p'.format(key) if how == 'pickle' else '{}.json'.format(key)
|
filename = os.path.join(folder, key + '.p')
|
||||||
filename = os.path.join(folder, name)
|
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
if how == 'pickle':
|
try:
|
||||||
with open(filename, 'rb') as handle:
|
with open(filename, 'rb') as handle:
|
||||||
return pickle.load(handle)
|
return pickle.load(handle)
|
||||||
|
except Exception as e:
|
||||||
else:
|
return None
|
||||||
with open(filename) as data_file:
|
|
||||||
data = json.load(data_file, cls=ExchangeJSONDecoder)
|
|
||||||
return data
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def save_algo_object(algo_name, key, obj, environ=None, rel_path=None,
|
def save_algo_object(algo_name, key, obj, environ=None, rel_path=None):
|
||||||
how='pickle'):
|
|
||||||
"""
|
"""
|
||||||
Serialize and save an object by algo name and key.
|
Serialize and save an object by algo name and key.
|
||||||
|
|
||||||
@@ -322,15 +321,10 @@ def save_algo_object(algo_name, key, obj, environ=None, rel_path=None,
|
|||||||
folder = os.path.join(folder, rel_path)
|
folder = os.path.join(folder, rel_path)
|
||||||
ensure_directory(folder)
|
ensure_directory(folder)
|
||||||
|
|
||||||
if how == 'json':
|
filename = os.path.join(folder, key + '.p')
|
||||||
filename = os.path.join(folder, '{}.json'.format(key))
|
|
||||||
with open(filename, 'wt') as handle:
|
|
||||||
json.dump(obj, handle, indent=4, cls=ExchangeJSONEncoder)
|
|
||||||
|
|
||||||
else:
|
with open(filename, 'wb') as handle:
|
||||||
filename = os.path.join(folder, '{}.p'.format(key))
|
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
with open(filename, 'wb') as handle:
|
|
||||||
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
|
||||||
|
|
||||||
|
|
||||||
def get_algo_df(algo_name, key, environ=None, rel_path=None):
|
def get_algo_df(algo_name, key, environ=None, rel_path=None):
|
||||||
@@ -434,15 +428,6 @@ def get_exchange_bundles_folder(exchange_name, environ=None):
|
|||||||
return temp_bundles
|
return temp_bundles
|
||||||
|
|
||||||
|
|
||||||
def has_bundle(exchange_name, data_frequency, environ=None):
|
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
|
||||||
|
|
||||||
folder_name = '{}_bundle'.format(data_frequency.lower())
|
|
||||||
folder = os.path.join(exchange_folder, folder_name)
|
|
||||||
|
|
||||||
return os.path.isdir(folder)
|
|
||||||
|
|
||||||
|
|
||||||
def symbols_serial(obj):
|
def symbols_serial(obj):
|
||||||
"""
|
"""
|
||||||
JSON serializer for objects not serializable by default json code
|
JSON serializer for objects not serializable by default json code
|
||||||
@@ -546,11 +531,6 @@ def get_frequency(freq, data_frequency):
|
|||||||
else:
|
else:
|
||||||
raise InvalidHistoryFrequencyError(frequency=freq)
|
raise InvalidHistoryFrequencyError(frequency=freq)
|
||||||
|
|
||||||
# TODO: some exchanges support H and W frequencies but not bundles
|
|
||||||
# Find a way to pass-through these parameters to exchanges
|
|
||||||
# but resample from minute or daily in backtest mode
|
|
||||||
# see catalyst/exchange/ccxt/ccxt_exchange.py:242 for mapping between
|
|
||||||
# Pandas offet aliases (used by Catalyst) and the CCXT timeframes
|
|
||||||
if unit.lower() == 'd':
|
if unit.lower() == 'd':
|
||||||
alias = '{}D'.format(candle_size)
|
alias = '{}D'.format(candle_size)
|
||||||
|
|
||||||
@@ -624,7 +604,6 @@ def mixin_market_params(exchange_name, params, market):
|
|||||||
# TODO: make this more externalized / configurable
|
# TODO: make this more externalized / configurable
|
||||||
if 'lot' in market:
|
if 'lot' in market:
|
||||||
params['min_trade_size'] = market['lot']
|
params['min_trade_size'] = market['lot']
|
||||||
params['lot'] = market['lot']
|
|
||||||
|
|
||||||
if exchange_name == 'bitfinex':
|
if exchange_name == 'bitfinex':
|
||||||
params['maker'] = 0.001
|
params['maker'] = 0.001
|
||||||
@@ -645,91 +624,6 @@ def mixin_market_params(exchange_name, params, market):
|
|||||||
if 'minimum_order_size' in info:
|
if 'minimum_order_size' in info:
|
||||||
params['min_trade_size'] = float(info['minimum_order_size'])
|
params['min_trade_size'] = float(info['minimum_order_size'])
|
||||||
|
|
||||||
if 'lot' not in params:
|
|
||||||
params['lot'] = params['min_trade_size']
|
|
||||||
|
|
||||||
|
|
||||||
def from_ms_timestamp(ms):
|
def from_ms_timestamp(ms):
|
||||||
return pd.to_datetime(ms, unit='ms', utc=True)
|
return pd.to_datetime(ms, unit='ms', utc=True)
|
||||||
|
|
||||||
|
|
||||||
def get_epoch():
|
|
||||||
return pd.to_datetime('1970-1-1', utc=True)
|
|
||||||
|
|
||||||
|
|
||||||
def group_assets_by_exchange(assets):
|
|
||||||
exchange_assets = dict()
|
|
||||||
for asset in assets:
|
|
||||||
if asset.exchange not in exchange_assets:
|
|
||||||
exchange_assets[asset.exchange] = list()
|
|
||||||
|
|
||||||
exchange_assets[asset.exchange].append(asset)
|
|
||||||
|
|
||||||
return exchange_assets
|
|
||||||
|
|
||||||
|
|
||||||
def get_catalyst_symbol(market_or_symbol):
|
|
||||||
"""
|
|
||||||
The Catalyst symbol.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
market_or_symbol
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
if isinstance(market_or_symbol, string_types):
|
|
||||||
parts = market_or_symbol.split('/')
|
|
||||||
return '{}_{}'.format(parts[0].lower(), parts[1].lower())
|
|
||||||
|
|
||||||
else:
|
|
||||||
return '{}_{}'.format(
|
|
||||||
market_or_symbol['base'].lower(),
|
|
||||||
market_or_symbol['quote'].lower(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def save_asset_data(folder, df, decimals=8):
|
|
||||||
symbols = df.index.get_level_values('symbol')
|
|
||||||
for symbol in symbols:
|
|
||||||
symbol_df = df.loc[(symbols == symbol)] # Type: pd.DataFrame
|
|
||||||
|
|
||||||
filename = os.path.join(folder, '{}.csv'.format(symbol))
|
|
||||||
if os.path.exists(filename):
|
|
||||||
print_headers = False
|
|
||||||
|
|
||||||
else:
|
|
||||||
print_headers = True
|
|
||||||
|
|
||||||
with open(filename, 'a') as f:
|
|
||||||
symbol_df.to_csv(
|
|
||||||
path_or_buf=f,
|
|
||||||
header=print_headers,
|
|
||||||
float_format='%.{}f'.format(decimals),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_candles_df(candles, field, freq, bar_count, end_dt,
|
|
||||||
previous_value=None):
|
|
||||||
all_series = dict()
|
|
||||||
for asset in candles:
|
|
||||||
periods = pd.date_range(end=end_dt, periods=bar_count, freq=freq)
|
|
||||||
|
|
||||||
dates = [candle['last_traded'] for candle in candles[asset]]
|
|
||||||
values = [candle[field] for candle in candles[asset]]
|
|
||||||
series = pd.Series(values, index=dates)
|
|
||||||
|
|
||||||
series = series.reindex(
|
|
||||||
periods,
|
|
||||||
method='ffill',
|
|
||||||
fill_value=previous_value,
|
|
||||||
)
|
|
||||||
series.sort_index(inplace=True)
|
|
||||||
all_series[asset] = series
|
|
||||||
|
|
||||||
df = pd.DataFrame(all_series)
|
|
||||||
df.dropna(inplace=True)
|
|
||||||
|
|
||||||
return df
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
||||||
|
from catalyst.exchange.exchange_errors import ExchangeAuthEmpty
|
||||||
|
from catalyst.exchange.exchange_utils import get_exchange_auth, \
|
||||||
|
get_exchange_folder
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange(exchange_name, base_currency=None, portfolio=None,
|
||||||
|
must_authenticate=False):
|
||||||
|
exchange_auth = get_exchange_auth(exchange_name)
|
||||||
|
|
||||||
|
has_auth = (exchange_auth['key'] != '' and exchange_auth['secret'] != '')
|
||||||
|
if must_authenticate and not has_auth:
|
||||||
|
raise ExchangeAuthEmpty(
|
||||||
|
exchange=exchange_name.title(),
|
||||||
|
filename=os.path.join(
|
||||||
|
get_exchange_folder(exchange_name), 'auth.json'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return CCXT(
|
||||||
|
exchange_name=exchange_name,
|
||||||
|
key=exchange_auth['key'],
|
||||||
|
secret=exchange_auth['secret'],
|
||||||
|
base_currency=base_currency,
|
||||||
|
portfolio=portfolio
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchanges(exchange_names):
|
||||||
|
exchanges = dict()
|
||||||
|
for exchange_name in exchange_names:
|
||||||
|
exchanges[exchange_name] = get_exchange(exchange_name)
|
||||||
|
|
||||||
|
return exchanges
|
||||||
@@ -6,7 +6,8 @@ from catalyst.gens.sim_engine import (
|
|||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.exchange.utils.stats_utils import prepare_stats
|
from catalyst.exchange.exchange_errors import \
|
||||||
|
MismatchingBaseCurrenciesExchanges
|
||||||
|
|
||||||
log = Logger('LiveGraphClock', level=LOG_LEVEL)
|
log = Logger('LiveGraphClock', level=LOG_LEVEL)
|
||||||
|
|
||||||
@@ -37,23 +38,177 @@ class LiveGraphClock(object):
|
|||||||
the exchange and the live trading machine's clock. It's not used currently.
|
the exchange and the live trading machine's clock. It's not used currently.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, sessions, context, callback=None,
|
def __init__(self, sessions, context, time_skew=pd.Timedelta('0s')):
|
||||||
time_skew=pd.Timedelta('0s')):
|
|
||||||
|
global mdates, plt # TODO: Could be cleaner
|
||||||
|
import matplotlib.dates as mdates
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
from matplotlib import style
|
||||||
|
|
||||||
self.sessions = sessions
|
self.sessions = sessions
|
||||||
self.time_skew = time_skew
|
self.time_skew = time_skew
|
||||||
self._last_emit = None
|
self._last_emit = None
|
||||||
self._before_trading_start_bar_yielded = True
|
self._before_trading_start_bar_yielded = True
|
||||||
self.context = context
|
self.context = context
|
||||||
self.callback = callback
|
self.fmt = mdates.DateFormatter('%Y-%m-%d %H:%M')
|
||||||
|
|
||||||
|
style.use('dark_background')
|
||||||
|
|
||||||
|
fig = plt.figure()
|
||||||
|
fig.canvas.set_window_title('Enigma Catalyst: {}'.format(
|
||||||
|
self.context.algo_namespace))
|
||||||
|
|
||||||
|
self.ax_pnl = fig.add_subplot(311)
|
||||||
|
|
||||||
|
self.ax_custom_signals = fig.add_subplot(312, sharex=self.ax_pnl)
|
||||||
|
|
||||||
|
self.ax_exposure = fig.add_subplot(313, sharex=self.ax_pnl)
|
||||||
|
|
||||||
|
if len(context.minute_stats) > 0:
|
||||||
|
self.draw_pnl()
|
||||||
|
self.draw_custom_signals()
|
||||||
|
self.draw_exposure()
|
||||||
|
|
||||||
|
# rotates and right aligns the x labels, and moves the bottom of the
|
||||||
|
# axes up to make room for them
|
||||||
|
fig.autofmt_xdate()
|
||||||
|
fig.subplots_adjust(hspace=0.5)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.ion()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
def format_ax(self, ax):
|
||||||
|
"""
|
||||||
|
Trying to assign reasonable parameters to the time axis.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ax:
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: room for improvement
|
||||||
|
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1))
|
||||||
|
ax.xaxis.set_major_formatter(self.fmt)
|
||||||
|
|
||||||
|
locator = mdates.HourLocator(interval=4)
|
||||||
|
locator.MAXTICKS = 5000
|
||||||
|
ax.xaxis.set_minor_locator(locator)
|
||||||
|
|
||||||
|
datemin = pd.Timestamp.utcnow()
|
||||||
|
ax.set_xlim(datemin)
|
||||||
|
|
||||||
|
ax.grid(True)
|
||||||
|
|
||||||
|
def set_legend(self, ax):
|
||||||
|
"""
|
||||||
|
Set legend on the chart.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ax
|
||||||
|
|
||||||
|
"""
|
||||||
|
ax.legend(loc='upper left', ncol=1, fontsize=10, numpoints=1)
|
||||||
|
|
||||||
|
def draw_pnl(self):
|
||||||
|
"""
|
||||||
|
Draw p&l line on the chart.
|
||||||
|
|
||||||
|
"""
|
||||||
|
ax = self.ax_pnl
|
||||||
|
df = self.context.pnl_stats
|
||||||
|
|
||||||
|
ax.clear()
|
||||||
|
ax.set_title('Performance')
|
||||||
|
ax.plot(df.index, df['performance'], '-',
|
||||||
|
color='green',
|
||||||
|
linewidth=1.0,
|
||||||
|
label='Performance'
|
||||||
|
)
|
||||||
|
|
||||||
|
def perc(val):
|
||||||
|
return '{:2f}'.format(val)
|
||||||
|
|
||||||
|
ax.format_ydata = perc
|
||||||
|
|
||||||
|
self.set_legend(ax)
|
||||||
|
self.format_ax(ax)
|
||||||
|
|
||||||
|
def draw_custom_signals(self):
|
||||||
|
"""
|
||||||
|
Draw custom signals on the chart.
|
||||||
|
|
||||||
|
"""
|
||||||
|
ax = self.ax_custom_signals
|
||||||
|
df = self.context.custom_signals_stats
|
||||||
|
|
||||||
|
colors = ['blue', 'green', 'red', 'black', 'orange', 'yellow', 'pink']
|
||||||
|
|
||||||
|
ax.clear()
|
||||||
|
ax.set_title('Custom Signals')
|
||||||
|
for index, column in enumerate(df.columns.values.tolist()):
|
||||||
|
ax.plot(df.index, df[column], '-',
|
||||||
|
color=colors[index],
|
||||||
|
linewidth=1.0,
|
||||||
|
label=column
|
||||||
|
)
|
||||||
|
|
||||||
|
self.set_legend(ax)
|
||||||
|
self.format_ax(ax)
|
||||||
|
|
||||||
|
def draw_exposure(self):
|
||||||
|
"""
|
||||||
|
Draw exposure line on the chart.
|
||||||
|
|
||||||
|
"""
|
||||||
|
ax = self.ax_exposure
|
||||||
|
context = self.context
|
||||||
|
df = context.exposure_stats
|
||||||
|
|
||||||
|
# TODO: list exchanges in graph
|
||||||
|
base_currency = None
|
||||||
|
positions = []
|
||||||
|
for exchange_name in context.exchanges:
|
||||||
|
exchange = context.exchanges[exchange_name]
|
||||||
|
|
||||||
|
if not base_currency:
|
||||||
|
base_currency = exchange.base_currency
|
||||||
|
elif base_currency != exchange.base_currency:
|
||||||
|
raise MismatchingBaseCurrenciesExchanges(
|
||||||
|
base_currency=base_currency,
|
||||||
|
exchange_name=exchange.name,
|
||||||
|
exchange_currency=exchange.base_currency
|
||||||
|
)
|
||||||
|
|
||||||
|
positions += exchange.portfolio.positions
|
||||||
|
|
||||||
|
ax.clear()
|
||||||
|
ax.set_title('Exposure')
|
||||||
|
ax.plot(df.index, df['base_currency'], '-',
|
||||||
|
color='green',
|
||||||
|
linewidth=1.0,
|
||||||
|
label='Base Currency: {}'.format(base_currency.upper())
|
||||||
|
)
|
||||||
|
|
||||||
|
symbols = []
|
||||||
|
for position in positions:
|
||||||
|
symbols.append(position.symbol)
|
||||||
|
|
||||||
|
ax.plot(df.index, df['long_exposure'], '-',
|
||||||
|
color='blue',
|
||||||
|
linewidth=1.0,
|
||||||
|
label='Long Exposure: {}'.format(', '.join(symbols).upper()))
|
||||||
|
|
||||||
|
self.set_legend(ax)
|
||||||
|
self.format_ax(ax)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
from matplotlib import pyplot as plt
|
|
||||||
yield pd.Timestamp.utcnow(), SESSION_START
|
yield pd.Timestamp.utcnow(), SESSION_START
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
current_time = pd.Timestamp.utcnow()
|
current_time = pd.Timestamp.utcnow()
|
||||||
current_minute = current_time.floor('1T')
|
current_minute = current_time.floor('1 min')
|
||||||
|
|
||||||
if self._last_emit is None or current_minute > self._last_emit:
|
if self._last_emit is None or current_minute > self._last_emit:
|
||||||
log.debug('emitting minutely bar: {}'.format(current_minute))
|
log.debug('emitting minutely bar: {}'.format(current_minute))
|
||||||
@@ -61,11 +216,14 @@ class LiveGraphClock(object):
|
|||||||
self._last_emit = current_minute
|
self._last_emit = current_minute
|
||||||
yield current_minute, BAR
|
yield current_minute, BAR
|
||||||
|
|
||||||
recorded_cols = list(self.context.recorded_vars.keys())
|
try:
|
||||||
df, _ = prepare_stats(
|
self.draw_pnl()
|
||||||
self.context.frame_stats, recorded_cols=recorded_cols
|
self.draw_custom_signals()
|
||||||
)
|
self.draw_exposure()
|
||||||
self.callback(self.context, df)
|
|
||||||
|
plt.draw()
|
||||||
|
except Exception as e:
|
||||||
|
log.warn('Unable to update the graph: {}'.format(e))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# I can't use the "animate" reactive approach here because
|
# I can't use the "animate" reactive approach here because
|
||||||
|
|||||||
@@ -0,0 +1,655 @@
|
|||||||
|
import json
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytz
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from logbook import Logger
|
||||||
|
# import six
|
||||||
|
from six import iteritems
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
# from websocket import create_connection
|
||||||
|
from catalyst.exchange.exchange import Exchange
|
||||||
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
|
from catalyst.exchange.exchange_errors import (
|
||||||
|
ExchangeRequestError,
|
||||||
|
InvalidHistoryFrequencyError,
|
||||||
|
InvalidOrderStyle, OrphanOrderReverseError)
|
||||||
|
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
|
||||||
|
ExchangeStopLimitOrder
|
||||||
|
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename, \
|
||||||
|
download_exchange_symbols, get_symbols_string
|
||||||
|
from catalyst.exchange.poloniex.poloniex_api import Poloniex_api
|
||||||
|
from catalyst.finance.order import Order, ORDER_STATUS
|
||||||
|
from catalyst.finance.transaction import Transaction
|
||||||
|
from catalyst.protocol import Account
|
||||||
|
|
||||||
|
log = Logger('Poloniex', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
|
class Poloniex(Exchange):
|
||||||
|
def __init__(self, key, secret, base_currency, portfolio=None):
|
||||||
|
self.api = Poloniex_api(key=key, secret=secret)
|
||||||
|
self.name = 'poloniex'
|
||||||
|
|
||||||
|
self.assets = dict()
|
||||||
|
self.load_assets()
|
||||||
|
|
||||||
|
self.local_assets = dict()
|
||||||
|
self.load_assets(is_local=True)
|
||||||
|
|
||||||
|
self.base_currency = base_currency
|
||||||
|
self._portfolio = portfolio
|
||||||
|
self.minute_writer = None
|
||||||
|
self.minute_reader = None
|
||||||
|
self.transactions = defaultdict(list)
|
||||||
|
|
||||||
|
self.num_candles_limit = 2000
|
||||||
|
self.max_requests_per_minute = 60
|
||||||
|
self.request_cpt = dict()
|
||||||
|
|
||||||
|
self.bundle = ExchangeBundle(self.name)
|
||||||
|
|
||||||
|
def sanitize_curency_symbol(self, exchange_symbol):
|
||||||
|
"""
|
||||||
|
Helper method used to build the universal pair.
|
||||||
|
Include any symbol mapping here if appropriate.
|
||||||
|
|
||||||
|
:param exchange_symbol:
|
||||||
|
:return universal_symbol:
|
||||||
|
"""
|
||||||
|
return exchange_symbol.lower()
|
||||||
|
|
||||||
|
def _create_order(self, order_status):
|
||||||
|
"""
|
||||||
|
Create a Catalyst order object from the Exchange order dictionary
|
||||||
|
:param order_status:
|
||||||
|
:return: Order
|
||||||
|
"""
|
||||||
|
# if order_status['is_cancelled']:
|
||||||
|
# status = ORDER_STATUS.CANCELLED
|
||||||
|
# elif not order_status['is_live']:
|
||||||
|
# log.info('found executed order {}'.format(order_status))
|
||||||
|
# status = ORDER_STATUS.FILLED
|
||||||
|
# else:
|
||||||
|
status = ORDER_STATUS.OPEN
|
||||||
|
|
||||||
|
amount = float(order_status['amount'])
|
||||||
|
# filled = float(order_status['executed_amount'])
|
||||||
|
filled = None
|
||||||
|
|
||||||
|
if order_status['type'] == 'sell':
|
||||||
|
amount = -amount
|
||||||
|
# filled = -filled
|
||||||
|
|
||||||
|
price = float(order_status['rate'])
|
||||||
|
order_type = order_status['type']
|
||||||
|
|
||||||
|
stop_price = None
|
||||||
|
limit_price = None
|
||||||
|
|
||||||
|
# TODO: is this comprehensive enough?
|
||||||
|
# if order_type.endswith('limit'):
|
||||||
|
# limit_price = price
|
||||||
|
# elif order_type.endswith('stop'):
|
||||||
|
# stop_price = price
|
||||||
|
|
||||||
|
# executed_price = float(order_status['avg_execution_price'])
|
||||||
|
executed_price = price
|
||||||
|
|
||||||
|
# TODO: bitfinex does not specify comission. I could calculate it but not sure if it's worth it.
|
||||||
|
commission = None
|
||||||
|
|
||||||
|
# date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp']))
|
||||||
|
# date = pytz.utc.localize(date)
|
||||||
|
date = None
|
||||||
|
|
||||||
|
order = Order(
|
||||||
|
dt=date,
|
||||||
|
asset=self.assets[order_status['symbol']],
|
||||||
|
# No such field in Poloniex
|
||||||
|
amount=amount,
|
||||||
|
stop=stop_price,
|
||||||
|
limit=limit_price,
|
||||||
|
filled=filled,
|
||||||
|
id=str(order_status['orderNumber']),
|
||||||
|
commission=commission
|
||||||
|
)
|
||||||
|
order.status = status
|
||||||
|
|
||||||
|
return order, executed_price
|
||||||
|
|
||||||
|
def get_balances(self):
|
||||||
|
balances = self.api.returnbalances()
|
||||||
|
try:
|
||||||
|
log.debug('retrieving wallets balances')
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(e)
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'error' in balances:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='unable to fetch balance {}'.format(balances['error'])
|
||||||
|
)
|
||||||
|
|
||||||
|
std_balances = dict()
|
||||||
|
for (key, value) in iteritems(balances):
|
||||||
|
currency = key.lower()
|
||||||
|
std_balances[currency] = float(value)
|
||||||
|
|
||||||
|
return std_balances
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account(self):
|
||||||
|
account = Account()
|
||||||
|
|
||||||
|
account.settled_cash = None
|
||||||
|
account.accrued_interest = None
|
||||||
|
account.buying_power = None
|
||||||
|
account.equity_with_loan = None
|
||||||
|
account.total_positions_value = None
|
||||||
|
account.total_positions_exposure = None
|
||||||
|
account.regt_equity = None
|
||||||
|
account.regt_margin = None
|
||||||
|
account.initial_margin_requirement = None
|
||||||
|
account.maintenance_margin_requirement = None
|
||||||
|
account.available_funds = None
|
||||||
|
account.excess_liquidity = None
|
||||||
|
account.cushion = None
|
||||||
|
account.day_trades_remaining = None
|
||||||
|
account.leverage = None
|
||||||
|
account.net_leverage = None
|
||||||
|
account.net_liquidation = None
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_skew(self):
|
||||||
|
# TODO: research the time skew conditions
|
||||||
|
return pd.Timedelta('0s')
|
||||||
|
|
||||||
|
def get_account(self):
|
||||||
|
# TODO: fetch account data and keep in cache
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_candles(self, freq, assets, bar_count=None,
|
||||||
|
start_dt=None, end_dt=None):
|
||||||
|
"""
|
||||||
|
Retrieve OHLVC candles from Poloniex
|
||||||
|
|
||||||
|
:param freq:
|
||||||
|
:param assets:
|
||||||
|
:param bar_count:
|
||||||
|
:return:
|
||||||
|
|
||||||
|
Available Frequencies
|
||||||
|
---------------------
|
||||||
|
'5m', '15m', '30m', '2h', '4h', '1D'
|
||||||
|
"""
|
||||||
|
|
||||||
|
if end_dt is None:
|
||||||
|
end_dt = pd.Timestamp.utcnow()
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
'retrieving {bars} {freq} candles on {exchange} from '
|
||||||
|
'{end_dt} for markets {symbols}, '.format(
|
||||||
|
bars=bar_count,
|
||||||
|
freq=freq,
|
||||||
|
exchange=self.name,
|
||||||
|
end_dt=end_dt,
|
||||||
|
symbols=get_symbols_string(assets)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if freq == '1T' and (bar_count == 1 or bar_count is None):
|
||||||
|
# TODO: use the order book instead
|
||||||
|
# We use the 5m to fetch the last bar
|
||||||
|
frequency = 300
|
||||||
|
elif freq == '5T':
|
||||||
|
frequency = 300
|
||||||
|
elif freq == '15T':
|
||||||
|
frequency = 900
|
||||||
|
elif freq == '30T':
|
||||||
|
frequency = 1800
|
||||||
|
elif freq == '120T':
|
||||||
|
frequency = 7200
|
||||||
|
elif freq == '240T':
|
||||||
|
frequency = 14400
|
||||||
|
elif freq == '1D':
|
||||||
|
frequency = 86400
|
||||||
|
else:
|
||||||
|
# Poloniex does not offer 1m data candles
|
||||||
|
# It is likely to error out there frequently
|
||||||
|
raise InvalidHistoryFrequencyError(frequency=freq)
|
||||||
|
|
||||||
|
# Making sure that assets are iterable
|
||||||
|
asset_list = [assets] if isinstance(assets, TradingPair) else assets
|
||||||
|
ohlc_map = dict()
|
||||||
|
|
||||||
|
for asset in asset_list:
|
||||||
|
delta = end_dt - pd.to_datetime('1970-1-1', utc=True)
|
||||||
|
end = int(delta.total_seconds())
|
||||||
|
|
||||||
|
if bar_count is None:
|
||||||
|
start = end - 2 * frequency
|
||||||
|
else:
|
||||||
|
start = end - bar_count * frequency
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.api.returnchartdata(
|
||||||
|
self.get_symbol(asset), frequency, start, end
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'error' in response:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='Unable to retrieve candles: {}'.format(
|
||||||
|
response.content)
|
||||||
|
)
|
||||||
|
|
||||||
|
def ohlc_from_candle(candle):
|
||||||
|
last_traded = pd.Timestamp.utcfromtimestamp(candle['date'])
|
||||||
|
last_traded = last_traded.replace(tzinfo=pytz.UTC)
|
||||||
|
|
||||||
|
ohlc = dict(
|
||||||
|
open=np.float64(candle['open']),
|
||||||
|
high=np.float64(candle['high']),
|
||||||
|
low=np.float64(candle['low']),
|
||||||
|
close=np.float64(candle['close']),
|
||||||
|
volume=np.float64(candle['volume']),
|
||||||
|
price=np.float64(candle['close']),
|
||||||
|
last_traded=last_traded
|
||||||
|
)
|
||||||
|
|
||||||
|
return ohlc
|
||||||
|
|
||||||
|
if bar_count is None:
|
||||||
|
ohlc_map[asset] = ohlc_from_candle(response[0])
|
||||||
|
else:
|
||||||
|
ohlc_bars = []
|
||||||
|
for candle in response:
|
||||||
|
ohlc = ohlc_from_candle(candle)
|
||||||
|
ohlc_bars.append(ohlc)
|
||||||
|
ohlc_map[asset] = ohlc_bars
|
||||||
|
|
||||||
|
return ohlc_map[assets] \
|
||||||
|
if isinstance(assets, TradingPair) else ohlc_map
|
||||||
|
|
||||||
|
def create_order(self, asset, amount, is_buy, style):
|
||||||
|
"""
|
||||||
|
Creating order on the exchange.
|
||||||
|
|
||||||
|
:param asset:
|
||||||
|
:param amount:
|
||||||
|
:param is_buy:
|
||||||
|
:param style:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
exchange_symbol = self.get_symbol(asset)
|
||||||
|
|
||||||
|
if isinstance(style, ExchangeLimitOrder) or isinstance(style,
|
||||||
|
ExchangeStopLimitOrder):
|
||||||
|
if isinstance(style, ExchangeStopLimitOrder):
|
||||||
|
log.warn('{} will ignore the stop price'.format(self.name))
|
||||||
|
|
||||||
|
price = style.get_limit_price(is_buy)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if (is_buy):
|
||||||
|
response = self.api.buy(exchange_symbol, amount, price)
|
||||||
|
else:
|
||||||
|
response = self.api.sell(exchange_symbol, -amount, price)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
date = pd.Timestamp.utcnow()
|
||||||
|
|
||||||
|
if ('orderNumber' in response):
|
||||||
|
order_id = str(response['orderNumber'])
|
||||||
|
order = Order(
|
||||||
|
dt=date,
|
||||||
|
asset=asset,
|
||||||
|
amount=amount,
|
||||||
|
stop=style.get_stop_price(is_buy),
|
||||||
|
limit=style.get_limit_price(is_buy),
|
||||||
|
id=order_id
|
||||||
|
)
|
||||||
|
return order
|
||||||
|
else:
|
||||||
|
log.warn(
|
||||||
|
'{} order failed: {}'.format('buy' if is_buy else 'sell',
|
||||||
|
response['error']))
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise InvalidOrderStyle(exchange=self.name,
|
||||||
|
style=style.__class__.__name__)
|
||||||
|
|
||||||
|
def get_open_orders(self, asset='all'):
|
||||||
|
"""Retrieve all of the current open orders.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asset : Asset
|
||||||
|
If passed and not 'all', return only the open orders for the given
|
||||||
|
asset instead of all open orders.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
open_orders : dict[list[Order]] or list[Order]
|
||||||
|
If 'all' is passed this will return a dict mapping Assets
|
||||||
|
to a list containing all the open orders for the asset.
|
||||||
|
If an asset is passed then this will return a list of the open
|
||||||
|
orders for this asset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.portfolio.open_orders
|
||||||
|
|
||||||
|
"""
|
||||||
|
TODO: Why going to the exchange if we already have this info locally?
|
||||||
|
And why creating all these Orders if we later discard them?
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if (asset == 'all'):
|
||||||
|
response = self.api.returnopenorders('all')
|
||||||
|
else:
|
||||||
|
response = self.api.returnopenorders(self.get_symbol(asset))
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'error' in response:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='Unable to retrieve open orders: {}'.format(
|
||||||
|
order_statuses['message'])
|
||||||
|
)
|
||||||
|
|
||||||
|
print(self.portfolio.open_orders)
|
||||||
|
|
||||||
|
# TODO: Need to handle openOrders for 'all'
|
||||||
|
orders = list()
|
||||||
|
for order_status in response:
|
||||||
|
order, executed_price = self._create_order(
|
||||||
|
order_status) # will Throw error b/c Polo doesn't track order['symbol']
|
||||||
|
if asset is None or asset == order.sid:
|
||||||
|
orders.append(order)
|
||||||
|
|
||||||
|
return orders
|
||||||
|
|
||||||
|
def get_order(self, order_id):
|
||||||
|
"""Lookup an order based on the order id returned from one of the
|
||||||
|
order functions.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
order_id : str
|
||||||
|
The unique identifier for the order.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
order : Order
|
||||||
|
The order object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
order = self._portfolio.open_orders[order_id]
|
||||||
|
except Exception as e:
|
||||||
|
raise OrphanOrderError(order_id=order_id, exchange=self.name)
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
# TODO: Need to decide whether we fetch orders locally or from exchnage
|
||||||
|
# The code below is ignored
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.api.returnopenorders(self.get_symbol(order.sid))
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
for o in response:
|
||||||
|
if (int(o['orderNumber']) == int(order_id)):
|
||||||
|
return order
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def cancel_order(self, order_param):
|
||||||
|
"""Cancel an open order.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
order_param : str or Order
|
||||||
|
The order_id or order object to cancel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if (isinstance(order_param, Order)):
|
||||||
|
order = order_param
|
||||||
|
else:
|
||||||
|
order = self._portfolio.open_orders[order_param]
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.api.cancelorder(order.id)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'error' in response:
|
||||||
|
log.info(
|
||||||
|
'Unable to cancel order {order_id} on exchange {exchange} {error}.'.format(
|
||||||
|
order_id=order.id,
|
||||||
|
exchange=self.name,
|
||||||
|
error=response['error']
|
||||||
|
))
|
||||||
|
|
||||||
|
# raise OrderCancelError(
|
||||||
|
# order_id=order.id,
|
||||||
|
# exchange=self.name,
|
||||||
|
# error=response['error']
|
||||||
|
# )
|
||||||
|
|
||||||
|
self.portfolio.remove_order(order)
|
||||||
|
|
||||||
|
def tickers(self, assets):
|
||||||
|
"""
|
||||||
|
Fetch ticket data for assets
|
||||||
|
https://docs.bitfinex.com/v2/reference#rest-public-tickers
|
||||||
|
|
||||||
|
:param assets:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
symbols = self.get_symbols(assets)
|
||||||
|
|
||||||
|
log.debug('fetching tickers {}'.format(symbols))
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.api.returnticker()
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'error' in response:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='Unable to retrieve tickers: {}'.format(
|
||||||
|
response['error'])
|
||||||
|
)
|
||||||
|
|
||||||
|
ticks = dict()
|
||||||
|
|
||||||
|
for index, symbol in enumerate(symbols):
|
||||||
|
ticks[assets[index]] = dict(
|
||||||
|
timestamp=pd.Timestamp.utcnow(),
|
||||||
|
bid=float(response[symbol]['highestBid']),
|
||||||
|
ask=float(response[symbol]['lowestAsk']),
|
||||||
|
last_price=float(response[symbol]['last']),
|
||||||
|
low=float(response[symbol]['lowestAsk']),
|
||||||
|
# TODO: Polo does not provide low
|
||||||
|
high=float(response[symbol]['highestBid']),
|
||||||
|
# TODO: Polo does not provide high
|
||||||
|
volume=float(response[symbol]['baseVolume']),
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug('got tickers {}'.format(ticks))
|
||||||
|
return ticks
|
||||||
|
|
||||||
|
def generate_symbols_json(self, filename=None, source_dates=False):
|
||||||
|
symbol_map = {}
|
||||||
|
|
||||||
|
if not source_dates:
|
||||||
|
fn, r = download_exchange_symbols(self.name)
|
||||||
|
with open(fn) as data_file:
|
||||||
|
cached_symbols = json.load(data_file)
|
||||||
|
|
||||||
|
response = self.api.returnticker()
|
||||||
|
|
||||||
|
for exchange_symbol in response:
|
||||||
|
base, market = self.sanitize_curency_symbol(exchange_symbol).split(
|
||||||
|
'_')
|
||||||
|
symbol = '{market}_{base}'.format(market=market, base=base)
|
||||||
|
|
||||||
|
if (source_dates):
|
||||||
|
start_date = self.get_symbol_start_date(exchange_symbol)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
start_date = cached_symbols[exchange_symbol]['start_date']
|
||||||
|
except KeyError as e:
|
||||||
|
start_date = time.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_daily = cached_symbols[exchange_symbol]['end_daily']
|
||||||
|
except KeyError as e:
|
||||||
|
end_daily = 'N/A'
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_minute = cached_symbols[exchange_symbol]['end_minute']
|
||||||
|
except KeyError as e:
|
||||||
|
end_minute = 'N/A'
|
||||||
|
|
||||||
|
symbol_map[exchange_symbol] = dict(
|
||||||
|
symbol=symbol,
|
||||||
|
start_date=start_date,
|
||||||
|
end_daily=end_daily,
|
||||||
|
end_minute=end_minute,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (filename is None):
|
||||||
|
filename = get_exchange_symbols_filename(self.name)
|
||||||
|
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(symbol_map, f, sort_keys=True, indent=2,
|
||||||
|
separators=(',', ':'))
|
||||||
|
|
||||||
|
def get_symbol_start_date(self, symbol):
|
||||||
|
try:
|
||||||
|
r = self.api.returnchartdata(symbol, 86400, pd.to_datetime(
|
||||||
|
'2010-1-1').value // 10 ** 9)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
return time.strftime('%Y-%m-%d', time.gmtime(int(r[0]['date'])))
|
||||||
|
|
||||||
|
def check_open_orders(self):
|
||||||
|
"""
|
||||||
|
Need to override this function for Poloniex:
|
||||||
|
|
||||||
|
Loop through the list of open orders in the Portfolio object.
|
||||||
|
Check if any transactions have been executed:
|
||||||
|
If so, create a transaction and apply to the Portfolio.
|
||||||
|
Check if the order is still open:
|
||||||
|
If not, remove it from open orders
|
||||||
|
|
||||||
|
:return:
|
||||||
|
transactions: Transaction[]
|
||||||
|
"""
|
||||||
|
transactions = list()
|
||||||
|
if self.portfolio.open_orders:
|
||||||
|
for order_id in list(self.portfolio.open_orders):
|
||||||
|
|
||||||
|
order = self._portfolio.open_orders[order_id]
|
||||||
|
log.debug('found open order: {}'.format(order_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
order_open = self.get_order(order_id)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if (order_open):
|
||||||
|
delta = pd.Timestamp.utcnow() - order.dt
|
||||||
|
log.info(
|
||||||
|
'order {order_id} still open after {delta}'.format(
|
||||||
|
order_id=order_id,
|
||||||
|
delta=delta)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.api.returnordertrades(order_id)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if ('error' in response):
|
||||||
|
if (not order_open):
|
||||||
|
raise OrphanOrderReverseError(order_id=order_id,
|
||||||
|
exchange=self.name)
|
||||||
|
else:
|
||||||
|
for tx in response:
|
||||||
|
"""
|
||||||
|
We maintain a list of dictionaries of transactions that correspond to
|
||||||
|
partially filled orders, indexed by order_id. Every time we query
|
||||||
|
executed transactions from the exchange, we check if we had that
|
||||||
|
transaction for that order already. If not, we process it.
|
||||||
|
|
||||||
|
When an order if fully filled, we flush the dict of transactions
|
||||||
|
associated with that order.
|
||||||
|
"""
|
||||||
|
if (not filter(
|
||||||
|
lambda item: item['order_id'] == tx['tradeID'],
|
||||||
|
self.transactions[order_id])):
|
||||||
|
log.debug(
|
||||||
|
'Got new transaction for order {}: amount {}, price {}'.format(
|
||||||
|
order_id, tx['amount'], tx['rate']))
|
||||||
|
tx['amount'] = float(tx['amount'])
|
||||||
|
if (tx['type'] == 'sell'):
|
||||||
|
tx['amount'] = -tx['amount']
|
||||||
|
transaction = Transaction(
|
||||||
|
asset=order.asset,
|
||||||
|
amount=tx['amount'],
|
||||||
|
dt=pd.to_datetime(tx['date'], utc=True),
|
||||||
|
price=float(tx['rate']),
|
||||||
|
order_id=tx['tradeID'],
|
||||||
|
# it's a misnomer, but keeping it for compatibility
|
||||||
|
commission=float(tx['fee'])
|
||||||
|
)
|
||||||
|
self.transactions[order_id].append(transaction)
|
||||||
|
self.portfolio.execute_transaction(transaction)
|
||||||
|
transactions.append(transaction)
|
||||||
|
|
||||||
|
if (not order_open):
|
||||||
|
"""
|
||||||
|
Since transactions have been executed individually
|
||||||
|
the only thing left to do is remove them from list of open_orders
|
||||||
|
"""
|
||||||
|
del self.portfolio.open_orders[order_id]
|
||||||
|
del self.transactions[order_id]
|
||||||
|
|
||||||
|
return transactions
|
||||||
|
|
||||||
|
def get_orderbook(self, asset, order_type='all'):
|
||||||
|
exchange_symbol = asset.exchange_symbol
|
||||||
|
data = self.api.returnOrderBook(market=exchange_symbol)
|
||||||
|
|
||||||
|
result = dict()
|
||||||
|
for order_type in data:
|
||||||
|
# TODO: filter by type
|
||||||
|
if order_type != 'asks' and order_type != 'bids':
|
||||||
|
continue
|
||||||
|
|
||||||
|
result[order_type] = []
|
||||||
|
for entry in data[order_type]:
|
||||||
|
if len(entry) == 2:
|
||||||
|
result[order_type].append(
|
||||||
|
dict(
|
||||||
|
rate=float(entry[0]),
|
||||||
|
quantity=float(entry[1])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
from six.moves import urllib
|
||||||
|
|
||||||
|
# Workaround for backwards compatibility
|
||||||
|
# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7
|
||||||
|
urlopen = urllib.request.urlopen
|
||||||
|
|
||||||
|
|
||||||
|
class Poloniex_api(object):
|
||||||
|
def __init__(self, key, secret):
|
||||||
|
self.key = key
|
||||||
|
self.secret = secret
|
||||||
|
|
||||||
|
self.max_requests_per_second = 6
|
||||||
|
self.request_cpt = dict()
|
||||||
|
|
||||||
|
self.public = ['returnTicker', 'return24Volume', 'returnOrderBook',
|
||||||
|
'returnTradeHistory', 'returnChartData',
|
||||||
|
'returnCurrencies', 'returnLoanOrders']
|
||||||
|
self.trading = ['returnBalances', 'returnCompleteBalances',
|
||||||
|
'returnDepositAddresses',
|
||||||
|
'generateNewAddress', 'returnDepositsWithdrawals',
|
||||||
|
'returnOpenOrders',
|
||||||
|
'returnTradeHistory', 'returnOrderTrades',
|
||||||
|
'buy', 'sell', 'cancelOrder', 'moveOrder',
|
||||||
|
'withdraw', 'returnFeeInfo',
|
||||||
|
'returnAvailableAccountBalances',
|
||||||
|
'returnTradableBalances', 'transferBalance',
|
||||||
|
'returnMarginAccountSummary', 'marginBuy',
|
||||||
|
'marginSell',
|
||||||
|
'getMarginPosition', 'closeMarginPosition',
|
||||||
|
'createLoanOffer',
|
||||||
|
'cancelLoanOffer', 'returnOpenLoanOffers',
|
||||||
|
'returnActiveLoans',
|
||||||
|
'returnLendingHistory', 'toggleAutoRenew']
|
||||||
|
|
||||||
|
def ask_request(self):
|
||||||
|
"""
|
||||||
|
Asks permission to issue a request to the exchange.
|
||||||
|
The primary purpose is to avoid hitting rate limits.
|
||||||
|
|
||||||
|
The application will pause if the maximum requests per minute
|
||||||
|
permitted by the exchange is exceeded.
|
||||||
|
|
||||||
|
:return boolean:
|
||||||
|
|
||||||
|
"""
|
||||||
|
now = time.time()
|
||||||
|
if not self.request_cpt:
|
||||||
|
self.request_cpt = dict()
|
||||||
|
self.request_cpt[now] = 0
|
||||||
|
return True
|
||||||
|
|
||||||
|
cpt_date = list(self.request_cpt.keys())[0]
|
||||||
|
cpt = self.request_cpt[cpt_date]
|
||||||
|
|
||||||
|
if now > cpt_date + 1:
|
||||||
|
self.request_cpt = dict()
|
||||||
|
self.request_cpt[now] = 0
|
||||||
|
return True
|
||||||
|
|
||||||
|
if cpt >= self.max_requests_per_second:
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
self.request_cpt = dict()
|
||||||
|
self.request_cpt[now] = 0
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.request_cpt[cpt_date] += 1
|
||||||
|
|
||||||
|
def query(self, method, req={}):
|
||||||
|
|
||||||
|
if method in self.public:
|
||||||
|
url = 'https://poloniex.com/public?command=' + method + '&' + \
|
||||||
|
urllib.parse.urlencode(req)
|
||||||
|
headers = {}
|
||||||
|
post_data = None
|
||||||
|
elif method in self.trading:
|
||||||
|
url = 'https://poloniex.com/tradingApi'
|
||||||
|
req['command'] = method
|
||||||
|
req['nonce'] = int(time.time() * 1000)
|
||||||
|
post_data = urllib.parse.urlencode(req)
|
||||||
|
|
||||||
|
signature = hmac.new(self.secret.encode('utf-8'),
|
||||||
|
post_data.encode('utf-8'),
|
||||||
|
hashlib.sha512).hexdigest()
|
||||||
|
headers = {'Sign': signature, 'Key': self.key}
|
||||||
|
|
||||||
|
post_data = post_data.encode('utf-8')
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'Method "' + method + '" not found in neither the Public API '
|
||||||
|
'or Trading API endpoints'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.ask_request()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=post_data,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
return json.loads(
|
||||||
|
urlopen(req, context=ssl._create_unverified_context()).read())
|
||||||
|
|
||||||
|
def returnticker(self):
|
||||||
|
return self.query('returnTicker', {})
|
||||||
|
|
||||||
|
def return24volume(self):
|
||||||
|
return self.query('return24Volume', {})
|
||||||
|
|
||||||
|
def returnOrderBook(self, market='all'):
|
||||||
|
return self.query('returnOrderBook', {'currencyPair': market})
|
||||||
|
|
||||||
|
def returntradehistory(self, market, start=None, end=None):
|
||||||
|
if (start is not None and end is not None):
|
||||||
|
return self.query('returntradehistory',
|
||||||
|
{'currencyPair': market, 'start': start,
|
||||||
|
'end': end})
|
||||||
|
else:
|
||||||
|
return self.query('returntradehistory', {'currencyPair': market})
|
||||||
|
|
||||||
|
def returnchartdata(self, market, period, start, end=9999999999):
|
||||||
|
return self.query('returnChartData',
|
||||||
|
{'currencyPair': market, 'period': period,
|
||||||
|
'start': start, 'end': end})
|
||||||
|
|
||||||
|
def returncurrencies(self):
|
||||||
|
return self.query('returnCurrencies', {})
|
||||||
|
|
||||||
|
def returnloadorders(self, market):
|
||||||
|
return self.query('returnLoanOrders', {'currency': market})
|
||||||
|
|
||||||
|
def returnbalances(self):
|
||||||
|
return self.query('returnBalances')
|
||||||
|
|
||||||
|
def returncompletebalances(self, account):
|
||||||
|
if (account):
|
||||||
|
return self.query('returnCompleteBalances', {'account': account})
|
||||||
|
else:
|
||||||
|
return self.query('returnCompleteBalances')
|
||||||
|
|
||||||
|
def returndepositaddresses(self):
|
||||||
|
return self.query('returnDepositAddresses')
|
||||||
|
|
||||||
|
def generatenewaddress(self, currency):
|
||||||
|
return self.query('generateNewAddress', {'currency': currency})
|
||||||
|
|
||||||
|
def returnDepositsWithdrawals(self, start, end):
|
||||||
|
return self.query('returnDepositsWithdrawals',
|
||||||
|
{'start': start, 'end': end})
|
||||||
|
|
||||||
|
def returnopenorders(self, market):
|
||||||
|
return self.query('returnOpenOrders', {'currencyPair': market})
|
||||||
|
|
||||||
|
def returntradehistory(self, market):
|
||||||
|
# TODO: optional start and/or end and limit
|
||||||
|
return self.query('returnTradeHistory', {'currencyPair': market})
|
||||||
|
|
||||||
|
def returnordertrades(self, ordernumber):
|
||||||
|
return self.query('returnOrderTrades', {'orderNumber': ordernumber})
|
||||||
|
|
||||||
|
def buy(self, market, amount, rate, fillorkill=0, immediateorcancel=0,
|
||||||
|
postonly=0):
|
||||||
|
if (fillorkill):
|
||||||
|
return self.query('buy', {'currencyPair': market, 'rate': rate,
|
||||||
|
'amount': amount,
|
||||||
|
'fillOrKill': fillorkill, })
|
||||||
|
elif (immediateorcancel):
|
||||||
|
return self.query('buy', {'currencyPair': market, 'rate': rate,
|
||||||
|
'amount': amount,
|
||||||
|
'immediateOrCancel': immediateorcancel, })
|
||||||
|
elif (postonly):
|
||||||
|
return self.query('buy', {'currencyPair': market, 'rate': rate,
|
||||||
|
'amount': amount,
|
||||||
|
'postOnly': postonly, })
|
||||||
|
else:
|
||||||
|
return self.query('buy', {'currencyPair': market, 'rate': rate,
|
||||||
|
'amount': amount, })
|
||||||
|
|
||||||
|
def sell(self, market, amount, rate, fillorkill=0, immediateorcancel=0,
|
||||||
|
postonly=0):
|
||||||
|
if (fillorkill):
|
||||||
|
return self.query('sell', {'currencyPair': market, 'rate': rate,
|
||||||
|
'amount': amount,
|
||||||
|
'fillOrKill': fillorkill, })
|
||||||
|
elif (immediateorcancel):
|
||||||
|
return self.query('sell', {'currencyPair': market, 'rate': rate,
|
||||||
|
'amount': amount,
|
||||||
|
'immediateOrCancel': immediateorcancel, })
|
||||||
|
elif (postonly):
|
||||||
|
return self.query('sell', {'currencyPair': market, 'rate': rate,
|
||||||
|
'amount': amount,
|
||||||
|
'postOnly': postonly, })
|
||||||
|
else:
|
||||||
|
return self.query('sell', {'currencyPair': market, 'rate': rate,
|
||||||
|
'amount': amount, })
|
||||||
|
|
||||||
|
def cancelorder(self, ordernumber):
|
||||||
|
return self.query('cancelOrder', {'orderNumber': ordernumber})
|
||||||
|
|
||||||
|
def withdraw(self, currency, quantity, address):
|
||||||
|
return self.query('withdraw',
|
||||||
|
{'currency': currency, 'amount': quantity,
|
||||||
|
'address': address})
|
||||||
|
|
||||||
|
def returnfeeinfo(self):
|
||||||
|
return self.query('returnFeeInfo')
|
||||||
@@ -31,8 +31,7 @@ class SimpleClock(object):
|
|||||||
This class is a drop-in replacement for
|
This class is a drop-in replacement for
|
||||||
:class:`zipline.gens.sim_engine.MinuteSimulationClock`.
|
:class:`zipline.gens.sim_engine.MinuteSimulationClock`.
|
||||||
|
|
||||||
This is a stripped down version because crypto exchanges run
|
This is a stripped down version because crypto exchanges run around the clock.
|
||||||
around the clock.
|
|
||||||
|
|
||||||
The :param:`time_skew` parameter represents the time difference between
|
The :param:`time_skew` parameter represents the time difference between
|
||||||
the Broker and the live trading machine's clock.
|
the Broker and the live trading machine's clock.
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import numbers
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
The `x`-series is defined as having crossed over `y`-series if the value
|
||||||
|
of `x` is greater than the value of `y` and the value of `x` was less than
|
||||||
|
the value of `y` on the bar immediately preceding the current bar.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
source: Series
|
||||||
|
target: Series
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(target, numbers.Number):
|
||||||
|
if source[-1] is np.nan or source[-2] is np.nan \
|
||||||
|
or target is np.nan:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if source[-1] >= target > source[-2]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
The `x`-series is defined as having crossed under `y`-series if the value
|
||||||
|
of `x` is less than the value of `y` and the value of `x` was greater than
|
||||||
|
the value of `y` on the bar immediately preceding the current bar.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
source: Series
|
||||||
|
target: Series
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(target, numbers.Number):
|
||||||
|
if source[-1] is np.nan or source[-2] is np.nan \
|
||||||
|
or target is np.nan:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if source[-1] < target <= source[-2]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Format and print the last few rows of a statistics DataFrame.
|
||||||
|
See the pyfolio project for the data structure.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
stats_df: DataFrame
|
||||||
|
num_rows: int
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
stats_df.set_index('period_close', drop=True, inplace=True)
|
||||||
|
stats_df.dropna(axis=1, how='all', inplace=True)
|
||||||
|
|
||||||
|
pd.set_option('display.expand_frame_repr', False)
|
||||||
|
pd.set_option('precision', 3)
|
||||||
|
pd.set_option('display.width', 1000)
|
||||||
|
pd.set_option('display.max_colwidth', 1000)
|
||||||
|
|
||||||
|
columns = ['starting_cash', 'ending_cash', 'portfolio_value',
|
||||||
|
'pnl', 'long_exposure', 'short_exposure', 'orders',
|
||||||
|
'transactions', 'positions']
|
||||||
|
|
||||||
|
if recorded_cols is not None:
|
||||||
|
for column in recorded_cols:
|
||||||
|
columns.append(column)
|
||||||
|
|
||||||
|
def format_positions(positions):
|
||||||
|
parts = []
|
||||||
|
for position in positions:
|
||||||
|
msg = '{amount:.2f}{base} cost basis {cost_basis:.4f}{quote}'.format(
|
||||||
|
amount=position['amount'],
|
||||||
|
base=position['sid'].base_currency,
|
||||||
|
cost_basis=position['cost_basis'],
|
||||||
|
quote=position['sid'].quote_currency
|
||||||
|
)
|
||||||
|
parts.append(msg)
|
||||||
|
return ', '.join(parts)
|
||||||
|
|
||||||
|
formatters = {
|
||||||
|
'orders': lambda orders: len(orders),
|
||||||
|
'transactions': lambda transactions: len(transactions),
|
||||||
|
'returns': lambda returns: "{0:.4f}".format(returns),
|
||||||
|
'positions': format_positions
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats_df.tail(num_rows).to_string(
|
||||||
|
columns=columns,
|
||||||
|
formatters=formatters
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def df_to_string(df):
|
||||||
|
"""
|
||||||
|
Create a formatted str representation of the DataFrame.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
df: DataFrame
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
pd.set_option('display.expand_frame_repr', False)
|
||||||
|
pd.set_option('precision', 8)
|
||||||
|
pd.set_option('display.width', 1000)
|
||||||
|
pd.set_option('display.max_colwidth', 1000)
|
||||||
|
|
||||||
|
return df.to_string()
|
||||||
|
|
||||||
|
|
||||||
|
def extract_transactions(perf):
|
||||||
|
"""
|
||||||
|
Compute indexes for buy and sell transactions
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
perf: DataFrame
|
||||||
|
The algo performance DataFrame.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DataFrame
|
||||||
|
A DataFrame of transactions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
trans_list = perf.transactions.values
|
||||||
|
all_trans = [t for sublist in trans_list for t in sublist]
|
||||||
|
all_trans.sort(key=lambda t: t['dt'])
|
||||||
|
|
||||||
|
transactions = pd.DataFrame(all_trans)
|
||||||
|
if not transactions.empty:
|
||||||
|
transactions.set_index('dt', inplace=True, drop=True)
|
||||||
|
return transactions
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
|
||||||
from catalyst.exchange.exchange import Exchange
|
|
||||||
from catalyst.exchange.exchange_errors import ExchangeAuthEmpty
|
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_auth, \
|
|
||||||
get_exchange_folder, is_blacklist
|
|
||||||
|
|
||||||
log = Logger('factory', level=LOG_LEVEL)
|
|
||||||
exchange_cache = dict()
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchange(exchange_name, base_currency=None, must_authenticate=False,
|
|
||||||
skip_init=False):
|
|
||||||
key = (exchange_name, base_currency)
|
|
||||||
if key in exchange_cache:
|
|
||||||
return exchange_cache[key]
|
|
||||||
|
|
||||||
exchange_auth = get_exchange_auth(exchange_name)
|
|
||||||
|
|
||||||
has_auth = (exchange_auth['key'] != '' and exchange_auth['secret'] != '')
|
|
||||||
if must_authenticate and not has_auth:
|
|
||||||
raise ExchangeAuthEmpty(
|
|
||||||
exchange=exchange_name.title(),
|
|
||||||
filename=os.path.join(
|
|
||||||
get_exchange_folder(exchange_name), 'auth.json'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
exchange = CCXT(
|
|
||||||
exchange_name=exchange_name,
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'],
|
|
||||||
base_currency=base_currency,
|
|
||||||
)
|
|
||||||
exchange_cache[key] = exchange
|
|
||||||
|
|
||||||
if not skip_init:
|
|
||||||
exchange.init()
|
|
||||||
|
|
||||||
return exchange
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchanges(exchange_names):
|
|
||||||
exchanges = dict()
|
|
||||||
for exchange_name in exchange_names:
|
|
||||||
exchanges[exchange_name] = get_exchange(exchange_name)
|
|
||||||
|
|
||||||
return exchanges
|
|
||||||
|
|
||||||
|
|
||||||
def find_exchanges(features=None, skip_blacklist=True, is_authenticated=False,
|
|
||||||
base_currency=None):
|
|
||||||
"""
|
|
||||||
Find exchanges filtered by a list of feature.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
features: str
|
|
||||||
The list of features.
|
|
||||||
|
|
||||||
skip_blacklist: bool
|
|
||||||
is_authenticated: bool
|
|
||||||
base_currency: bool
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
list[Exchange]
|
|
||||||
|
|
||||||
"""
|
|
||||||
exchange_names = CCXT.find_exchanges(features, is_authenticated)
|
|
||||||
|
|
||||||
exchanges = []
|
|
||||||
for exchange_name in exchange_names:
|
|
||||||
if skip_blacklist and is_blacklist(exchange_name):
|
|
||||||
continue
|
|
||||||
|
|
||||||
exchange = get_exchange(
|
|
||||||
exchange_name=exchange_name,
|
|
||||||
skip_init=True,
|
|
||||||
base_currency=base_currency,
|
|
||||||
)
|
|
||||||
|
|
||||||
if features is not None:
|
|
||||||
if 'dailyBundle' in features \
|
|
||||||
and not exchange.has_bundle('daily'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif 'minuteBundle' in features \
|
|
||||||
and not exchange.has_bundle('minute'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
exchanges.append(exchange)
|
|
||||||
|
|
||||||
return exchanges
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import matplotlib.dates as mdates
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from catalyst.exchange.exchange_errors import \
|
|
||||||
MismatchingBaseCurrenciesExchanges
|
|
||||||
|
|
||||||
fmt = mdates.DateFormatter('%Y-%m-%d %H:%M')
|
|
||||||
|
|
||||||
|
|
||||||
def format_ax(ax):
|
|
||||||
"""
|
|
||||||
Trying to assign reasonable parameters to the time axis.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
ax:
|
|
||||||
|
|
||||||
"""
|
|
||||||
# TODO: room for improvement
|
|
||||||
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1))
|
|
||||||
ax.xaxis.set_major_formatter(fmt)
|
|
||||||
|
|
||||||
locator = mdates.HourLocator(interval=4)
|
|
||||||
locator.MAXTICKS = 5000
|
|
||||||
ax.xaxis.set_minor_locator(locator)
|
|
||||||
|
|
||||||
datemin = pd.Timestamp.utcnow()
|
|
||||||
ax.set_xlim(datemin)
|
|
||||||
|
|
||||||
ax.grid(True)
|
|
||||||
|
|
||||||
|
|
||||||
def set_legend(ax):
|
|
||||||
"""
|
|
||||||
Set legend on the chart.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
ax
|
|
||||||
|
|
||||||
"""
|
|
||||||
ax.legend(loc='upper left', ncol=1, fontsize=10, numpoints=1)
|
|
||||||
|
|
||||||
|
|
||||||
def draw_pnl(ax, df):
|
|
||||||
"""
|
|
||||||
Draw p&l line on the chart.
|
|
||||||
|
|
||||||
"""
|
|
||||||
ax.clear()
|
|
||||||
ax.set_title('Performance')
|
|
||||||
index = df.index.unique()
|
|
||||||
dt = index.get_level_values(level=0)
|
|
||||||
pnl = index.get_level_values(level=4)
|
|
||||||
ax.plot(
|
|
||||||
dt, pnl, '-',
|
|
||||||
color='green',
|
|
||||||
linewidth=1.0,
|
|
||||||
label='Performance'
|
|
||||||
)
|
|
||||||
|
|
||||||
def perc(val):
|
|
||||||
return '{:2f}'.format(val)
|
|
||||||
|
|
||||||
ax.format_ydata = perc
|
|
||||||
|
|
||||||
set_legend(ax)
|
|
||||||
format_ax(ax)
|
|
||||||
|
|
||||||
|
|
||||||
def draw_custom_signals(ax, df):
|
|
||||||
"""
|
|
||||||
Draw custom signals on the chart.
|
|
||||||
|
|
||||||
"""
|
|
||||||
colors = ['blue', 'green', 'red', 'black', 'orange', 'yellow', 'pink']
|
|
||||||
|
|
||||||
ax.clear()
|
|
||||||
ax.set_title('Custom Signals')
|
|
||||||
for index, column in enumerate(df.columns.values.tolist()):
|
|
||||||
ax.plot(df.index, df[column], '-',
|
|
||||||
color=colors[index],
|
|
||||||
linewidth=1.0,
|
|
||||||
label=column
|
|
||||||
)
|
|
||||||
|
|
||||||
set_legend(ax)
|
|
||||||
format_ax(ax)
|
|
||||||
|
|
||||||
|
|
||||||
def draw_exposure(ax, df, context):
|
|
||||||
"""
|
|
||||||
Draw exposure line on the chart.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# TODO: list exchanges in graph
|
|
||||||
base_currency = None
|
|
||||||
positions = []
|
|
||||||
for exchange_name in context.exchanges:
|
|
||||||
exchange = context.exchanges[exchange_name]
|
|
||||||
|
|
||||||
if not base_currency:
|
|
||||||
base_currency = exchange.base_currency
|
|
||||||
elif base_currency != exchange.base_currency:
|
|
||||||
raise MismatchingBaseCurrenciesExchanges(
|
|
||||||
base_currency=base_currency,
|
|
||||||
exchange_name=exchange.name,
|
|
||||||
exchange_currency=exchange.base_currency
|
|
||||||
)
|
|
||||||
|
|
||||||
positions += exchange.portfolio.positions
|
|
||||||
|
|
||||||
ax.clear()
|
|
||||||
ax.set_title('Exposure')
|
|
||||||
ax.plot(df.index, df['base_currency'], '-',
|
|
||||||
color='green',
|
|
||||||
linewidth=1.0,
|
|
||||||
label='Base Currency: {}'.format(base_currency.upper())
|
|
||||||
)
|
|
||||||
|
|
||||||
symbols = []
|
|
||||||
for position in positions:
|
|
||||||
symbols.append(position.symbol)
|
|
||||||
|
|
||||||
ax.plot(df.index, df['long_exposure'], '-',
|
|
||||||
color='blue',
|
|
||||||
linewidth=1.0,
|
|
||||||
label='Long Exposure: {}'.format(', '.join(symbols).upper()))
|
|
||||||
|
|
||||||
set_legend(ax)
|
|
||||||
format_ax(ax)
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import json
|
|
||||||
import re
|
|
||||||
from json import JSONEncoder
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
from six import string_types
|
|
||||||
|
|
||||||
from catalyst.constants import DATE_TIME_FORMAT
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeJSONEncoder(json.JSONEncoder):
|
|
||||||
def default(self, obj):
|
|
||||||
if isinstance(obj, pd.Timestamp):
|
|
||||||
return obj.strftime(DATE_TIME_FORMAT)
|
|
||||||
|
|
||||||
# Let the base class default method raise the TypeError
|
|
||||||
return JSONEncoder.default(self, obj)
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeJSONDecoder(json.JSONDecoder):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
json.JSONDecoder.__init__(
|
|
||||||
self, object_hook=self.object_hook, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def recursive_iter(self, obj):
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
for key, value in obj.items():
|
|
||||||
match = isinstance(value, string_types) and re.search(
|
|
||||||
r'(\d{4}-\d{2}-\d{2}).*', value
|
|
||||||
)
|
|
||||||
if match:
|
|
||||||
try:
|
|
||||||
obj[key] = pd.to_datetime(value, utc=True)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
elif any(isinstance(obj, t) for t in (list, tuple)):
|
|
||||||
for item in obj:
|
|
||||||
self.recursive_iter(item)
|
|
||||||
|
|
||||||
def object_hook(self, obj):
|
|
||||||
self.recursive_iter(obj)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
def portfolio_to_dict(portfolio):
|
|
||||||
positions = []
|
|
||||||
for asset in portfolio.positions:
|
|
||||||
p = portfolio.positions[asset] # Type: Position
|
|
||||||
|
|
||||||
position = dict(
|
|
||||||
symbol=asset.symbol,
|
|
||||||
exchange=asset.exchange,
|
|
||||||
amount=p.amount,
|
|
||||||
cost_basis=p.cost_basis,
|
|
||||||
last_sale_price=p.last_sale_price,
|
|
||||||
last_sale_date=p.last_sale_date,
|
|
||||||
)
|
|
||||||
positions.append(position)
|
|
||||||
|
|
||||||
portfolio_dict = vars(portfolio)
|
|
||||||
portfolio_dict['positions'] = positions
|
|
||||||
|
|
||||||
return portfolio_dict
|
|
||||||
|
|
||||||
|
|
||||||
def portfolio_from_dict(self, portfolio_data):
|
|
||||||
from catalyst.protocol import Portfolio
|
|
||||||
return Portfolio()
|
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
import copy
|
|
||||||
import csv
|
|
||||||
import json
|
|
||||||
import numbers
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
|
|
||||||
from catalyst.exchange.utils.exchange_utils import get_algo_folder
|
|
||||||
from catalyst.utils.paths import data_root, ensure_directory
|
|
||||||
|
|
||||||
s3_conn = []
|
|
||||||
mailgun = []
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
The `x`-series is defined as having crossed over `y`-series if the value
|
|
||||||
of `x` is greater than the value of `y` and the value of `x` was less than
|
|
||||||
the value of `y` on the bar immediately preceding the current bar.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
source: Series
|
|
||||||
target: Series
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
bool
|
|
||||||
|
|
||||||
"""
|
|
||||||
if isinstance(target, numbers.Number):
|
|
||||||
if source[-1] is np.nan or source[-2] is np.nan \
|
|
||||||
or target is np.nan:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if source[-1] >= target > source[-2]:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
else:
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
The `x`-series is defined as having crossed under `y`-series if the value
|
|
||||||
of `x` is less than the value of `y` and the value of `x` was greater than
|
|
||||||
the value of `y` on the bar immediately preceding the current bar.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
source: Series
|
|
||||||
target: Series
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
bool
|
|
||||||
|
|
||||||
"""
|
|
||||||
if isinstance(target, numbers.Number):
|
|
||||||
if source[-1] is np.nan or source[-2] is np.nan \
|
|
||||||
or target is np.nan:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if source[-1] < target <= source[-2]:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
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 set_position_row(row, asset, asset_values=list()):
|
|
||||||
"""
|
|
||||||
Apply the position data as individual columns.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
row: dict[str, Object]
|
|
||||||
asset: TradingPair
|
|
||||||
asset_values: list[str]
|
|
||||||
If a recorded_col contains a tuple which first value is an asset
|
|
||||||
matching a position, its value will be displayed with the
|
|
||||||
position and not in the index.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
asset_cols = ['symbol']
|
|
||||||
row['symbol'] = asset.symbol
|
|
||||||
|
|
||||||
position = next((p for p in row['positions'] if p['sid'] == asset), None)
|
|
||||||
|
|
||||||
columns = ['amount', 'cost_basis', 'last_sale_price']
|
|
||||||
for column in columns:
|
|
||||||
if position is not None:
|
|
||||||
row[column] = position[column]
|
|
||||||
|
|
||||||
else:
|
|
||||||
row[column] = 0
|
|
||||||
|
|
||||||
asset_cols.append(column)
|
|
||||||
|
|
||||||
values = asset_values[asset] if asset in asset_values else list()
|
|
||||||
for column in values:
|
|
||||||
row[column] = values[column]
|
|
||||||
|
|
||||||
asset_cols.append(column)
|
|
||||||
|
|
||||||
return asset_cols
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_stats(stats, recorded_cols=list()):
|
|
||||||
"""
|
|
||||||
Prepare the stats DataFrame for user-friendly output.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
stats: list[Object]
|
|
||||||
recorded_cols: list[str]
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
asset_cols = list()
|
|
||||||
|
|
||||||
stats = copy.deepcopy(stats)
|
|
||||||
# Using a copy since we are adding rows inside the loop.
|
|
||||||
for row_index, row_data in enumerate(list(stats)):
|
|
||||||
assets = [p['sid'] for p in row_data['positions']]
|
|
||||||
|
|
||||||
asset_values = dict()
|
|
||||||
if recorded_cols is not None:
|
|
||||||
for column in recorded_cols[:]:
|
|
||||||
value = row_data[column]
|
|
||||||
if isinstance(value, pd.Series):
|
|
||||||
value = value.to_dict()
|
|
||||||
|
|
||||||
if type(value) is dict:
|
|
||||||
for asset in value:
|
|
||||||
if not isinstance(asset, TradingPair):
|
|
||||||
break
|
|
||||||
|
|
||||||
if asset not in assets:
|
|
||||||
assets.append(asset)
|
|
||||||
|
|
||||||
if asset not in asset_values:
|
|
||||||
asset_values[asset] = dict()
|
|
||||||
|
|
||||||
asset_values[asset][column] = value[asset]
|
|
||||||
|
|
||||||
if len(assets) == 1:
|
|
||||||
row = stats[row_index]
|
|
||||||
asset_cols = set_position_row(row, assets[0], asset_values)
|
|
||||||
|
|
||||||
elif len(assets) > 1:
|
|
||||||
for asset_index, asset in enumerate(assets):
|
|
||||||
if asset_index > 0:
|
|
||||||
row = copy.deepcopy(row_data)
|
|
||||||
stats.append(row)
|
|
||||||
|
|
||||||
else:
|
|
||||||
row = stats[row_index]
|
|
||||||
|
|
||||||
asset_cols = set_position_row(row, assets[asset_index],
|
|
||||||
asset_values)
|
|
||||||
|
|
||||||
df = pd.DataFrame(stats)
|
|
||||||
|
|
||||||
index_cols = [
|
|
||||||
'period_close', 'starting_cash', 'ending_cash', 'portfolio_value',
|
|
||||||
'pnl', 'long_exposure', 'short_exposure', 'orders', 'transactions',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Removing the asset specific entries
|
|
||||||
if recorded_cols is not None:
|
|
||||||
recorded_cols = [x for x in recorded_cols if x not in asset_cols]
|
|
||||||
for column in recorded_cols:
|
|
||||||
index_cols.append(column)
|
|
||||||
|
|
||||||
df['orders'] = df['orders'].apply(lambda orders: len(orders))
|
|
||||||
df['transactions'] = df['transactions'].apply(
|
|
||||||
lambda transactions: len(transactions)
|
|
||||||
)
|
|
||||||
|
|
||||||
if asset_cols:
|
|
||||||
columns = asset_cols
|
|
||||||
df.set_index(index_cols, drop=True, inplace=True)
|
|
||||||
|
|
||||||
else:
|
|
||||||
columns = index_cols
|
|
||||||
columns.remove('period_close')
|
|
||||||
df.set_index('period_close', drop=False, inplace=True)
|
|
||||||
|
|
||||||
df.dropna(axis=1, how='all', inplace=True)
|
|
||||||
df.sort_index(axis=0, level=0, inplace=True)
|
|
||||||
|
|
||||||
return df, columns
|
|
||||||
|
|
||||||
|
|
||||||
def get_pretty_stats(stats, recorded_cols=None, num_rows=10):
|
|
||||||
"""
|
|
||||||
Format and print the last few rows of a statistics DataFrame.
|
|
||||||
See the pyfolio project for the data structure.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
stats: list[Object]
|
|
||||||
An array of statistics for the period.
|
|
||||||
|
|
||||||
num_rows: int
|
|
||||||
The number of rows to display on the screen.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
|
|
||||||
"""
|
|
||||||
if isinstance(stats, pd.DataFrame):
|
|
||||||
stats = stats.T.to_dict().values()
|
|
||||||
|
|
||||||
display_stats = stats[-num_rows:] if len(stats) > num_rows else stats
|
|
||||||
df, columns = prepare_stats(
|
|
||||||
display_stats, recorded_cols=recorded_cols
|
|
||||||
)
|
|
||||||
|
|
||||||
pd.set_option('display.expand_frame_repr', False)
|
|
||||||
pd.set_option('precision', 8)
|
|
||||||
pd.set_option('display.width', 1000)
|
|
||||||
pd.set_option('display.max_colwidth', 1000)
|
|
||||||
|
|
||||||
return df.to_string(columns=columns)
|
|
||||||
|
|
||||||
|
|
||||||
def get_csv_stats(stats, recorded_cols=None):
|
|
||||||
"""
|
|
||||||
Create a CSV buffer from the stats DataFrame.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
path: str
|
|
||||||
stats: list[Object]
|
|
||||||
recorded_cols: list[str]
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
df, columns = prepare_stats(stats, recorded_cols=recorded_cols)
|
|
||||||
|
|
||||||
return df.to_csv(
|
|
||||||
None,
|
|
||||||
columns=columns,
|
|
||||||
# encoding='utf-8',
|
|
||||||
quoting=csv.QUOTE_NONNUMERIC
|
|
||||||
).encode()
|
|
||||||
|
|
||||||
|
|
||||||
def stats_to_s3(uri, stats, algo_namespace, recorded_cols=None,
|
|
||||||
folder='catalyst/stats', bytes_to_write=None):
|
|
||||||
"""
|
|
||||||
Uploads the performance stats to a S3 bucket.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
uri: str
|
|
||||||
stats: list[Object]
|
|
||||||
algo_namespace: str
|
|
||||||
recorded_cols: list[str]
|
|
||||||
folder: str
|
|
||||||
bytes_to_write: str
|
|
||||||
Option to reuse bytes instead of re-computing the csv
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not s3_conn:
|
|
||||||
import boto3
|
|
||||||
s3_conn.append(boto3.resource('s3'))
|
|
||||||
|
|
||||||
s3 = s3_conn[0]
|
|
||||||
|
|
||||||
if bytes_to_write is None:
|
|
||||||
bytes_to_write = get_csv_stats(stats, recorded_cols=recorded_cols)
|
|
||||||
|
|
||||||
now = pd.Timestamp.utcnow()
|
|
||||||
timestr = now.strftime('%Y%m%d')
|
|
||||||
pid = os.getpid()
|
|
||||||
|
|
||||||
parts = uri.split('//')
|
|
||||||
obj = s3.Object(parts[1], '{}/{}-{}-{}.csv'.format(
|
|
||||||
folder, timestr, algo_namespace, pid
|
|
||||||
))
|
|
||||||
obj.put(Body=bytes_to_write)
|
|
||||||
|
|
||||||
|
|
||||||
def email_error(algo_name, dt, e, environ=None):
|
|
||||||
import requests
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
if not mailgun:
|
|
||||||
root = data_root(environ)
|
|
||||||
filename = os.path.join(root, 'mailgun.json')
|
|
||||||
if not os.path.exists(filename):
|
|
||||||
raise ValueError(
|
|
||||||
'mailgun.json not found in the catalyst data folder'
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(filename) as data_file:
|
|
||||||
mailgun.append(json.load(data_file))
|
|
||||||
|
|
||||||
mg = mailgun[0]
|
|
||||||
|
|
||||||
return requests.post(
|
|
||||||
mg['url'],
|
|
||||||
auth=("api", mg['api']),
|
|
||||||
data={
|
|
||||||
"from": mg['from'],
|
|
||||||
"to": mg['to'],
|
|
||||||
"subject": 'Error: {}'.format(algo_name),
|
|
||||||
"text": '{}\n\n{}\n{}'.format(
|
|
||||||
dt, e, traceback.format_exc()
|
|
||||||
)})
|
|
||||||
|
|
||||||
|
|
||||||
def stats_to_algo_folder(stats, algo_namespace, recorded_cols=None):
|
|
||||||
"""
|
|
||||||
Saves the performance stats to the algo local folder.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
stats: list[Object]
|
|
||||||
algo_namespace: str
|
|
||||||
recorded_cols: list[str]
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
|
|
||||||
"""
|
|
||||||
bytes_to_write = get_csv_stats(stats, recorded_cols=recorded_cols)
|
|
||||||
|
|
||||||
timestr = time.strftime('%Y%m%d')
|
|
||||||
folder = get_algo_folder(algo_namespace)
|
|
||||||
|
|
||||||
stats_folder = os.path.join(folder, 'stats')
|
|
||||||
ensure_directory(stats_folder)
|
|
||||||
|
|
||||||
filename = os.path.join(stats_folder, '{}.csv'.format(timestr))
|
|
||||||
|
|
||||||
with open(filename, 'wb') as handle:
|
|
||||||
handle.write(bytes_to_write)
|
|
||||||
|
|
||||||
return bytes_to_write
|
|
||||||
|
|
||||||
|
|
||||||
def df_to_string(df):
|
|
||||||
"""
|
|
||||||
Create a formatted str representation of the DataFrame.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
df: DataFrame
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
|
|
||||||
"""
|
|
||||||
pd.set_option('display.expand_frame_repr', False)
|
|
||||||
pd.set_option('precision', 8)
|
|
||||||
pd.set_option('display.width', 1000)
|
|
||||||
pd.set_option('display.max_colwidth', 1000)
|
|
||||||
|
|
||||||
return df.to_string()
|
|
||||||
|
|
||||||
|
|
||||||
def extract_transactions(perf):
|
|
||||||
"""
|
|
||||||
Compute indexes for buy and sell transactions
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
perf: DataFrame
|
|
||||||
The algo performance DataFrame.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
DataFrame
|
|
||||||
A DataFrame of transactions.
|
|
||||||
|
|
||||||
"""
|
|
||||||
trans_list = perf.transactions.values
|
|
||||||
all_trans = [t for sublist in trans_list for t in sublist]
|
|
||||||
all_trans.sort(key=lambda t: t['dt'])
|
|
||||||
|
|
||||||
transactions = pd.DataFrame(all_trans)
|
|
||||||
if not transactions.empty:
|
|
||||||
transactions.set_index('dt', inplace=True, drop=True)
|
|
||||||
return transactions
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import os
|
|
||||||
import random
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
|
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
|
||||||
from catalyst.exchange.utils.factory import find_exchanges
|
|
||||||
from catalyst.utils.paths import ensure_directory
|
|
||||||
|
|
||||||
|
|
||||||
def handle_exchange_error(exchange, e):
|
|
||||||
try:
|
|
||||||
message = '{}: {}'.format(
|
|
||||||
e.__class__, e.message.decode('ascii', 'ignore')
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
message = 'unexpected error'
|
|
||||||
|
|
||||||
folder = get_exchange_folder(exchange.name)
|
|
||||||
filename = os.path.join(folder, 'blacklist.txt')
|
|
||||||
with open(filename, 'wt') as handle:
|
|
||||||
handle.write(message)
|
|
||||||
|
|
||||||
|
|
||||||
def select_random_exchanges(population=3, features=None,
|
|
||||||
is_authenticated=False, base_currency=None):
|
|
||||||
all_exchanges = find_exchanges(
|
|
||||||
features=features,
|
|
||||||
is_authenticated=is_authenticated,
|
|
||||||
base_currency=base_currency,
|
|
||||||
)
|
|
||||||
|
|
||||||
if population is not None:
|
|
||||||
if len(all_exchanges) < population:
|
|
||||||
population = len(all_exchanges)
|
|
||||||
|
|
||||||
exchanges = random.sample(all_exchanges, population)
|
|
||||||
|
|
||||||
else:
|
|
||||||
exchanges = all_exchanges
|
|
||||||
|
|
||||||
return exchanges
|
|
||||||
|
|
||||||
|
|
||||||
def select_random_assets(all_assets, population=3):
|
|
||||||
assets = random.sample(all_assets, population)
|
|
||||||
return assets
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -15,9 +15,14 @@
|
|||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
|
||||||
from numpy import isfinite
|
from sys import float_info
|
||||||
|
|
||||||
from six import with_metaclass
|
from six import with_metaclass
|
||||||
|
|
||||||
|
import catalyst.utils.math_utils as zp_math
|
||||||
|
|
||||||
|
from numpy import isfinite
|
||||||
|
|
||||||
from catalyst.errors import BadOrderParameters
|
from catalyst.errors import BadOrderParameters
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,15 +27,15 @@ from .risk import (
|
|||||||
choose_treasury
|
choose_treasury
|
||||||
)
|
)
|
||||||
|
|
||||||
from catalyst.patches.stats import (
|
from empyrical import (
|
||||||
alpha_beta_aligned,
|
alpha_beta_aligned,
|
||||||
annual_volatility,
|
annual_volatility,
|
||||||
|
cum_returns,
|
||||||
downside_risk,
|
downside_risk,
|
||||||
information_ratio,
|
information_ratio,
|
||||||
max_drawdown,
|
max_drawdown,
|
||||||
sharpe_ratio,
|
sharpe_ratio,
|
||||||
sortino_ratio,
|
sortino_ratio,
|
||||||
cum_returns,
|
|
||||||
)
|
)
|
||||||
import warnings
|
import warnings
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
@@ -161,13 +161,9 @@ class RiskMetricsCumulative(object):
|
|||||||
if len(self.algorithm_returns) == 1:
|
if len(self.algorithm_returns) == 1:
|
||||||
self.algorithm_returns = np.append(0.0, self.algorithm_returns)
|
self.algorithm_returns = np.append(0.0, self.algorithm_returns)
|
||||||
|
|
||||||
try:
|
self.algorithm_cumulative_returns[dt_loc] = cum_returns(
|
||||||
self.algorithm_cumulative_returns[dt_loc] = cum_returns(
|
self.algorithm_returns
|
||||||
self.algorithm_returns
|
)[-1]
|
||||||
)[-1]
|
|
||||||
except Exception as e:
|
|
||||||
log.debug('unable to calculate cum returns: {}'.format(e))
|
|
||||||
self.algorithm_cumulative_returns[dt_loc] = np.nan
|
|
||||||
|
|
||||||
algo_cumulative_returns_to_date = \
|
algo_cumulative_returns_to_date = \
|
||||||
self.algorithm_cumulative_returns[:dt_loc + 1]
|
self.algorithm_cumulative_returns[:dt_loc + 1]
|
||||||
@@ -200,11 +196,8 @@ class RiskMetricsCumulative(object):
|
|||||||
self.benchmark_cumulative_returns[dt_loc] = cum_returns(
|
self.benchmark_cumulative_returns[dt_loc] = cum_returns(
|
||||||
self.benchmark_returns
|
self.benchmark_returns
|
||||||
)[-1]
|
)[-1]
|
||||||
except Exception as e:
|
except Exception:
|
||||||
log.debug(
|
self.benchmark_cumulative_returns[dt_loc] = 0
|
||||||
'unable to calculate benchmark cum returns: {}'.format(e)
|
|
||||||
)
|
|
||||||
self.benchmark_cumulative_returns[dt_loc] = np.nan
|
|
||||||
|
|
||||||
benchmark_cumulative_returns_to_date = \
|
benchmark_cumulative_returns_to_date = \
|
||||||
self.benchmark_cumulative_returns[:dt_loc + 1]
|
self.benchmark_cumulative_returns[:dt_loc + 1]
|
||||||
@@ -276,16 +269,9 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
|
|||||||
self.sharpe[dt_loc] = sharpe_ratio(
|
self.sharpe[dt_loc] = sharpe_ratio(
|
||||||
self.algorithm_returns,
|
self.algorithm_returns,
|
||||||
)
|
)
|
||||||
|
self.downside_risk[dt_loc] = downside_risk(
|
||||||
try:
|
self.algorithm_returns
|
||||||
self.downside_risk[dt_loc] = downside_risk(
|
)
|
||||||
self.algorithm_returns
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
log.debug(
|
|
||||||
'unable to calculate downside risk returns: {}'.format(e)
|
|
||||||
)
|
|
||||||
self.downside_risk[dt_loc] = np.nan
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
risk = self.downside_risk[dt_loc]
|
risk = self.downside_risk[dt_loc]
|
||||||
@@ -293,26 +279,17 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
|
|||||||
self.algorithm_returns,
|
self.algorithm_returns,
|
||||||
_downside_risk=risk
|
_downside_risk=risk
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
log.debug(
|
# TODO: what causes it to error out?
|
||||||
'unable to calculate benchmark cum returns: {}'.format(e)
|
self.sortino[dt_loc] = 0
|
||||||
)
|
|
||||||
self.sortino[dt_loc] = np.nan
|
|
||||||
|
|
||||||
self.information[dt_loc] = information_ratio(
|
self.information[dt_loc] = information_ratio(
|
||||||
self.algorithm_returns,
|
self.algorithm_returns,
|
||||||
self.benchmark_returns,
|
self.benchmark_returns,
|
||||||
)
|
)
|
||||||
try:
|
self.max_drawdown = max_drawdown(
|
||||||
self.max_drawdown = max_drawdown(
|
self.algorithm_returns
|
||||||
self.algorithm_returns
|
)
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
log.debug(
|
|
||||||
'unable to calculate max drawdown: {}'.format(e)
|
|
||||||
)
|
|
||||||
self.max_drawdown = np.nan
|
|
||||||
|
|
||||||
self.max_drawdowns[dt_loc] = self.max_drawdown
|
self.max_drawdowns[dt_loc] = self.max_drawdown
|
||||||
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
|
||||||
|
|||||||
@@ -154,8 +154,8 @@ class RiskMetricsPeriod(object):
|
|||||||
self.algorithm_returns.values,
|
self.algorithm_returns.values,
|
||||||
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()
|
||||||
|
|
||||||
|
|||||||
@@ -160,8 +160,7 @@ def choose_treasury(select_treasury, treasury_curves, start_session,
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Supress warning for 'OPEN' calendar
|
if search_day and trading_calendar.name != 'OPEN': # Supress warning for 'OPEN' calendar
|
||||||
if search_day and trading_calendar.name != 'OPEN':
|
|
||||||
if (search_dist is None or search_dist > 1) and \
|
if (search_dist is None or search_dist > 1) and \
|
||||||
search_days[0] <= end_session <= search_days[-1]:
|
search_days[0] <= end_session <= search_days[-1]:
|
||||||
message = "No rate within 1 trading day of end date = \
|
message = "No rate within 1 trading day of end date = \
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ DEFAULT_EQUITY_VOLUME_SLIPPAGE_BAR_LIMIT = 0.025
|
|||||||
DEFAULT_FUTURE_VOLUME_SLIPPAGE_BAR_LIMIT = 0.05
|
DEFAULT_FUTURE_VOLUME_SLIPPAGE_BAR_LIMIT = 0.05
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LiquidityExceeded(Exception):
|
class LiquidityExceeded(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@ from abc import (
|
|||||||
)
|
)
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import six
|
|
||||||
from six import (
|
from six import (
|
||||||
iteritems,
|
iteritems,
|
||||||
with_metaclass,
|
with_metaclass,
|
||||||
@@ -34,6 +33,7 @@ from catalyst.utils.sharedoc import copydoc
|
|||||||
|
|
||||||
|
|
||||||
class PipelineEngine(with_metaclass(ABCMeta)):
|
class PipelineEngine(with_metaclass(ABCMeta)):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def run_pipeline(self, pipeline, start_date, end_date):
|
def run_pipeline(self, pipeline, start_date, end_date):
|
||||||
"""
|
"""
|
||||||
@@ -118,7 +118,6 @@ class ExplodingPipelineEngine(PipelineEngine):
|
|||||||
"""
|
"""
|
||||||
A PipelineEngine that doesn't do anything.
|
A PipelineEngine that doesn't do anything.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def run_pipeline(self, pipeline, start_date, end_date):
|
def run_pipeline(self, pipeline, start_date, end_date):
|
||||||
raise NoEngineRegistered(
|
raise NoEngineRegistered(
|
||||||
"Attempted to run a pipeline but no pipeline "
|
"Attempted to run a pipeline but no pipeline "
|
||||||
@@ -485,10 +484,8 @@ class SimplePipelineEngine(PipelineEngine):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(term, LoadableTerm):
|
if isinstance(term, LoadableTerm):
|
||||||
term_key = loader_group_key(term)
|
|
||||||
# TODO: temp workaround
|
|
||||||
to_load = sorted(
|
to_load = sorted(
|
||||||
six.next(six.itervalues(loader_groups)),
|
loader_groups[loader_group_key(term)],
|
||||||
key=lambda t: t.dataset
|
key=lambda t: t.dataset
|
||||||
)
|
)
|
||||||
loader = get_loader(term)
|
loader = get_loader(term)
|
||||||
@@ -568,10 +565,9 @@ class SimplePipelineEngine(PipelineEngine):
|
|||||||
index=MultiIndex.from_arrays([empty_dates, empty_assets]),
|
index=MultiIndex.from_arrays([empty_dates, empty_assets]),
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: not sure what's wrong with the resolved_assets
|
resolved_assets = array(self._finder.retrieve_all(assets))
|
||||||
# resolved_assets = array(self._finder.retrieve_all(assets))
|
|
||||||
dates_kept = repeat_last_axis(dates.values, len(assets))[mask]
|
dates_kept = repeat_last_axis(dates.values, len(assets))[mask]
|
||||||
assets_kept = repeat_first_axis(assets, len(dates))[mask]
|
assets_kept = repeat_first_axis(resolved_assets, len(dates))[mask]
|
||||||
|
|
||||||
final_columns = {}
|
final_columns = {}
|
||||||
for name in data:
|
for name in data:
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
from .statistical import (
|
from .statistical import (
|
||||||
|
RollingPearson,
|
||||||
|
RollingLinearRegression,
|
||||||
RollingLinearRegressionOfReturns,
|
RollingLinearRegressionOfReturns,
|
||||||
RollingPearsonOfReturns,
|
RollingPearsonOfReturns,
|
||||||
|
RollingSpearman,
|
||||||
RollingSpearmanOfReturns,
|
RollingSpearmanOfReturns,
|
||||||
)
|
)
|
||||||
from .technical import (
|
from .technical import (
|
||||||
|
|||||||
@@ -38,11 +38,9 @@ class USEquityPricingLoader(PipelineLoader):
|
|||||||
|
|
||||||
def __init__(self, bundle, data_frequency, dataset):
|
def __init__(self, bundle, data_frequency, dataset):
|
||||||
|
|
||||||
# TODO: This is currently broken, No Pipeline support for Catalyst
|
if data_frequency == 'daily':
|
||||||
# if data_frequency == 'daily':
|
reader = bundle.daily_bar_reader
|
||||||
# reader = bundle.daily_bar_reader
|
elif daily_bar_reader == 'minute':
|
||||||
# elif daily_bar_reader == 'minute':
|
|
||||||
if data_frequency == 'minute':
|
|
||||||
reader = bundle.minute_bar_reader
|
reader = bundle.minute_bar_reader
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -53,9 +51,7 @@ class USEquityPricingLoader(PipelineLoader):
|
|||||||
|
|
||||||
if data_frequency == 'daily':
|
if data_frequency == 'daily':
|
||||||
all_sessions = cal.all_sessions
|
all_sessions = cal.all_sessions
|
||||||
# TODO: this cannot be right, but no pipeline support at the moment
|
elif daily_bar_reader == 'minute':
|
||||||
# elif daily_bar_reader == 'minute':
|
|
||||||
elif data_frequency == 'minute':
|
|
||||||
reader = bundle.minute_bar_reader
|
reader = bundle.minute_bar_reader
|
||||||
all_sessions = cal.all_minutes
|
all_sessions = cal.all_minutes
|
||||||
|
|
||||||
|
|||||||
@@ -180,3 +180,4 @@ class DataFrameLoader(PipelineLoader):
|
|||||||
@property
|
@property
|
||||||
def columns(self):
|
def columns(self):
|
||||||
return self._columns
|
return self._columns
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
from catalyst import run_algorithm
|
|
||||||
from catalyst.api import order, record, symbol
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from catalyst.exchange.utils.stats_utils import get_pretty_stats
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
context.assets = [symbol('eth_btc'), symbol('eth_usdt')]
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
order(context.assets[0], 1)
|
|
||||||
|
|
||||||
prices = data.current(context.assets, 'price')
|
|
||||||
record(price=prices)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def analyze(context, perf):
|
|
||||||
stats = get_pretty_stats(perf)
|
|
||||||
print(stats)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
live = True
|
|
||||||
if live:
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=0.01,
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
exchange_name='poloniex',
|
|
||||||
algo_namespace='buy_btc_polo_jh',
|
|
||||||
base_currency='btc',
|
|
||||||
analyze=analyze,
|
|
||||||
live=True,
|
|
||||||
simulate_orders=True,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=1000,
|
|
||||||
data_frequency='daily',
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
exchange_name='poloniex',
|
|
||||||
algo_namespace='buy_btc_polo_jh',
|
|
||||||
base_currency='usd',
|
|
||||||
analyze=analyze,
|
|
||||||
start=pd.to_datetime('2017-01-01', utc=True),
|
|
||||||
end=pd.to_datetime('2017-12-25', utc=True),
|
|
||||||
)
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
from catalyst.api import symbol
|
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
context.asset = symbol('bcc_usdt')
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
data.history(context.asset, ['close'], bar_count=100, frequency='5T')
|
|
||||||
|
|
||||||
|
|
||||||
def analyze(context=None, results=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=100,
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='bittrex',
|
|
||||||
algo_namespace="bittrex_is_broken",
|
|
||||||
base_currency='usdt',
|
|
||||||
data_frequency='minute',
|
|
||||||
simulate_orders=True,
|
|
||||||
live=True)
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import pandas as pd
|
||||||
|
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
|
||||||
|
context.base_currency = 'btc'
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
lookback = 60 * 24 * 7 # (minutes, hours, days)
|
||||||
|
context.i += 1
|
||||||
|
if context.i < lookback:
|
||||||
|
return
|
||||||
|
|
||||||
|
today = context.blotter.current_dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# update universe everyday
|
||||||
|
new_day = 60 * 24
|
||||||
|
if not context.i % new_day:
|
||||||
|
context.universe = universe(context, today)
|
||||||
|
|
||||||
|
# get data every 30 minutes
|
||||||
|
minutes = 30
|
||||||
|
if not context.i % minutes and context.universe:
|
||||||
|
for coin in context.coins:
|
||||||
|
pair = str(coin.symbol)
|
||||||
|
|
||||||
|
# ohlcv data
|
||||||
|
open = data.history(coin, 'open', lookback,
|
||||||
|
'1m').ffill().bfill().resample(
|
||||||
|
'30T').first()
|
||||||
|
high = data.history(coin, 'high', lookback,
|
||||||
|
'1m').ffill().bfill().resample('30T').max()
|
||||||
|
low = data.history(coin, 'low', lookback,
|
||||||
|
'1m').ffill().bfill().resample('30T').min()
|
||||||
|
close = data.history(coin, 'price', lookback,
|
||||||
|
'1m').ffill().bfill().resample(
|
||||||
|
'30T').last()
|
||||||
|
volume = data.history(coin, 'volume', lookback,
|
||||||
|
'1m').ffill().bfill().resample(
|
||||||
|
'30T').sum()
|
||||||
|
|
||||||
|
print(today, pair, close[-1])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, results=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def universe(context, today):
|
||||||
|
json_symbols = get_exchange_symbols('poloniex')
|
||||||
|
poloniex_universe_df = pd.DataFrame.from_dict(
|
||||||
|
json_symbols).transpose().astype(str)
|
||||||
|
poloniex_universe_df['base_currency'] = poloniex_universe_df.apply(
|
||||||
|
lambda row: row.symbol.split('_')[1],
|
||||||
|
axis=1)
|
||||||
|
poloniex_universe_df['market_currency'] = poloniex_universe_df.apply(
|
||||||
|
lambda row: row.symbol.split('_')[0],
|
||||||
|
axis=1)
|
||||||
|
poloniex_universe_df = poloniex_universe_df[
|
||||||
|
poloniex_universe_df['base_currency'] == context.base_currency]
|
||||||
|
poloniex_universe_df = poloniex_universe_df[
|
||||||
|
poloniex_universe_df.symbol != 'gas_btc']
|
||||||
|
|
||||||
|
# Markets currently not working on Catalyst 0.3.1
|
||||||
|
# 2017-01-01
|
||||||
|
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'bcn_btc']
|
||||||
|
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'burst_btc']
|
||||||
|
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'dgb_btc']
|
||||||
|
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'doge_btc']
|
||||||
|
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'emc2_btc']
|
||||||
|
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'pink_btc']
|
||||||
|
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'sc_btc']
|
||||||
|
print(poloniex_universe_df.head())
|
||||||
|
|
||||||
|
date = str(today).split(' ')[0]
|
||||||
|
|
||||||
|
poloniex_universe_df = poloniex_universe_df[
|
||||||
|
poloniex_universe_df.start_date < date]
|
||||||
|
context.coins = symbols(*poloniex_universe_df.symbol)
|
||||||
|
print(len(poloniex_universe_df))
|
||||||
|
return poloniex_universe_df.symbol.tolist()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
start_date = pd.to_datetime('2017-01-01', utc=True)
|
||||||
|
end_date = pd.to_datetime('2017-10-15', utc=True)
|
||||||
|
|
||||||
|
performance = run_algorithm(start=start_date, end=end_date,
|
||||||
|
capital_base=10000.0,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
data_frequency='minute',
|
||||||
|
base_currency='btc',
|
||||||
|
live=False,
|
||||||
|
live_graph=False,
|
||||||
|
algo_namespace='test')
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""
|
||||||
|
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 = 'poloniex' # must match the exchange specified in run_algorithm
|
||||||
|
context.base_currency = 'btc' # must match the base currency specified in run_algorithm
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
lookback = 60 * 24 * 7 # (minutes, hours, days) of how far to lookback in the data history
|
||||||
|
context.i += 1
|
||||||
|
|
||||||
|
# current date formatted into a string
|
||||||
|
today = context.blotter.current_dt
|
||||||
|
date, time = today.strftime('%Y-%m-%d %H:%M:%S').split(' ')
|
||||||
|
lookback_date = today - timedelta(days=(
|
||||||
|
lookback / (60 * 24))) # 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
|
||||||
|
if not context.i % new_day:
|
||||||
|
context.universe = universe(context, lookback_date, date)
|
||||||
|
|
||||||
|
# get data every 30 minutes
|
||||||
|
minutes = 30
|
||||||
|
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
|
||||||
|
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-01-08', utc=True)
|
||||||
|
end_date = pd.to_datetime('2017-11-13', utc=True)
|
||||||
|
|
||||||
|
performance = run_algorithm(start=start_date, end=end_date,
|
||||||
|
capital_base=10000.0,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
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
|
||||||
|
"""
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import talib
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
from catalyst import run_algorithm
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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, )
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from logbook import Logger, DEBUG
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import (schedule_function, order_target_percent, symbol,
|
||||||
|
date_rules, get_open_orders, cancel_order, record,
|
||||||
|
set_commission, set_slippage)
|
||||||
|
|
||||||
|
log = Logger('rodrigo_1', level=DEBUG)
|
||||||
|
"""
|
||||||
|
The initialize function sets any data or variables that
|
||||||
|
you'll use in your algorithm.
|
||||||
|
It's only called once at the beginning of your algorithm.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
# Select asset of interest
|
||||||
|
context.asset = symbol('BTC_USD')
|
||||||
|
|
||||||
|
# set_commission(TradingPairFeeSchedule(maker_fee=0.5, taker_fee=0.5))
|
||||||
|
# set_slippage(TradingPairFixedSlippage(spread=0.5))
|
||||||
|
# Set up a rebalance method to run every day
|
||||||
|
schedule_function(rebalance, date_rule=date_rules.every_day())
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Rebalance function scheduled to run once per day.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def rebalance(context, data):
|
||||||
|
# To make market decisions, we're calculating the token's
|
||||||
|
# moving average for the last 5 days.
|
||||||
|
|
||||||
|
# We get the price history for the last 5 days.
|
||||||
|
price_history = data.history(context.asset, fields='price', bar_count=5,
|
||||||
|
frequency='1d')
|
||||||
|
|
||||||
|
# Then we take an average of those 5 days.
|
||||||
|
average_price = price_history.mean()
|
||||||
|
|
||||||
|
# We also get the coin's current price.
|
||||||
|
price = data.current(context.asset, 'price')
|
||||||
|
|
||||||
|
# Cancel any outstanding orders
|
||||||
|
orders = get_open_orders(context.asset) or []
|
||||||
|
for order in orders:
|
||||||
|
cancel_order(order)
|
||||||
|
|
||||||
|
# If our coin is currently listed on a major exchange
|
||||||
|
if data.can_trade(context.asset):
|
||||||
|
# If the current price is 1% above the 5-day average price,
|
||||||
|
# we open a long position. If the current price is below the
|
||||||
|
# average price, then we want to close our position to 0 shares.
|
||||||
|
if price > (1.01 * average_price):
|
||||||
|
# Place the buy order (positive means buy, negative means sell)
|
||||||
|
order_target_percent(context.asset, .99)
|
||||||
|
log.info("Buying %s" % (context.asset.symbol))
|
||||||
|
elif price < average_price:
|
||||||
|
# Sell all of our shares by setting the target position to zero
|
||||||
|
order_target_percent(context.asset, 0)
|
||||||
|
log.info("Selling %s" % (context.asset.symbol))
|
||||||
|
|
||||||
|
# Use the record() method to track up to five custom signals.
|
||||||
|
# Record Apple's current price and the average price over the last
|
||||||
|
# five days.
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
leverage = context.account.leverage
|
||||||
|
|
||||||
|
record(price=price, average_price=average_price, cash=cash,
|
||||||
|
leverage=leverage)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, results=None):
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
# Plot the portfolio and asset data.
|
||||||
|
ax1 = plt.subplot(511)
|
||||||
|
results[['portfolio_value']].plot(ax=ax1)
|
||||||
|
ax1.set_ylabel('Portfolio Value (USD)')
|
||||||
|
|
||||||
|
ax2 = plt.subplot(512, sharex=ax1)
|
||||||
|
ax2.set_ylabel('{asset} (USD)'.format(asset=context.asset))
|
||||||
|
(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]
|
||||||
|
]
|
||||||
|
sells = trans.ix[
|
||||||
|
[t[0]['amount'] < 0 for t in trans.transactions]
|
||||||
|
]
|
||||||
|
|
||||||
|
ax2.plot(
|
||||||
|
buys.index,
|
||||||
|
results.price[buys.index],
|
||||||
|
'^',
|
||||||
|
markersize=10,
|
||||||
|
color='g',
|
||||||
|
)
|
||||||
|
ax2.plot(
|
||||||
|
sells.index,
|
||||||
|
results.price[sells.index],
|
||||||
|
'v',
|
||||||
|
markersize=10,
|
||||||
|
color='r',
|
||||||
|
)
|
||||||
|
|
||||||
|
ax3 = plt.subplot(513, sharex=ax1)
|
||||||
|
results[['leverage']].plot(ax=ax3)
|
||||||
|
ax3.set_ylabel('Leverage ')
|
||||||
|
|
||||||
|
ax4 = plt.subplot(514, sharex=ax1)
|
||||||
|
results[['cash']].plot(ax=ax4)
|
||||||
|
ax4.set_ylabel('Cash (USD)')
|
||||||
|
|
||||||
|
results[[
|
||||||
|
'algorithm',
|
||||||
|
'benchmark',
|
||||||
|
]] = results[[
|
||||||
|
'algorithm_period_return',
|
||||||
|
'benchmark_period_return',
|
||||||
|
]]
|
||||||
|
|
||||||
|
ax5 = plt.subplot(515, sharex=ax1)
|
||||||
|
results[[
|
||||||
|
'algorithm',
|
||||||
|
'benchmark',
|
||||||
|
]].plot(ax=ax5)
|
||||||
|
ax5.set_ylabel('Percent Change')
|
||||||
|
|
||||||
|
plt.legend(loc=3)
|
||||||
|
|
||||||
|
# Show the plot.
|
||||||
|
plt.gcf().set_size_inches(18, 8)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=100000,
|
||||||
|
start=pd.to_datetime('2017-1-1', utc=True),
|
||||||
|
end=pd.to_datetime('2017-10-22', utc=True),
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=None,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bitfinex',
|
||||||
|
algo_namespace='rodrigo_1',
|
||||||
|
base_currency='usd'
|
||||||
|
)
|
||||||
@@ -55,4 +55,4 @@ from .core import ( # noqa
|
|||||||
write_bcolz_minute_data,
|
write_bcolz_minute_data,
|
||||||
write_compressed,
|
write_compressed,
|
||||||
)
|
)
|
||||||
from .fixtures import ZiplineTestCase # noqa
|
from .fixtures import CatalystTestCase # noqa
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ def create_data_portal_from_trade_history(asset_finder, trading_calendar,
|
|||||||
return DataPortal(
|
return DataPortal(
|
||||||
asset_finder, trading_calendar,
|
asset_finder, trading_calendar,
|
||||||
first_trading_day=equity_daily_reader.first_trading_day,
|
first_trading_day=equity_daily_reader.first_trading_day,
|
||||||
equity_daily_reader=equity_daily_reader,
|
daily_reader=equity_daily_reader,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
minutes = trading_calendar.minutes_in_range(
|
minutes = trading_calendar.minutes_in_range(
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ from catalyst.utils.paths import ensure_directory
|
|||||||
catalyst_dir = os.path.dirname(catalyst.__file__)
|
catalyst_dir = os.path.dirname(catalyst.__file__)
|
||||||
|
|
||||||
|
|
||||||
class ZiplineTestCase(with_metaclass(FinalMeta, TestCase)):
|
class CatalystTestCase(with_metaclass(FinalMeta, TestCase)):
|
||||||
"""
|
"""
|
||||||
Shared extensions to core unittest.TestCase.
|
Shared extensions to core unittest.TestCase.
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class ZiplineTestCase(with_metaclass(FinalMeta, TestCase)):
|
|||||||
cls._base_init_fixtures_was_called = False
|
cls._base_init_fixtures_was_called = False
|
||||||
cls.init_class_fixtures()
|
cls.init_class_fixtures()
|
||||||
assert cls._base_init_fixtures_was_called, (
|
assert cls._base_init_fixtures_was_called, (
|
||||||
"ZiplineTestCase.init_class_fixtures() was not called.\n"
|
"CatalystTestCase.init_class_fixtures() was not called.\n"
|
||||||
"This probably means that you overrode init_class_fixtures"
|
"This probably means that you overrode init_class_fixtures"
|
||||||
" without calling super()."
|
" without calling super()."
|
||||||
)
|
)
|
||||||
@@ -170,7 +170,7 @@ class ZiplineTestCase(with_metaclass(FinalMeta, TestCase)):
|
|||||||
self._init_instance_fixtures_was_called = False
|
self._init_instance_fixtures_was_called = False
|
||||||
self.init_instance_fixtures()
|
self.init_instance_fixtures()
|
||||||
assert self._init_instance_fixtures_was_called, (
|
assert self._init_instance_fixtures_was_called, (
|
||||||
"ZiplineTestCase.init_instance_fixtures() was not"
|
"CatalystTestCase.init_instance_fixtures() was not"
|
||||||
" called.\n"
|
" called.\n"
|
||||||
"This probably means that you overrode"
|
"This probably means that you overrode"
|
||||||
" init_instance_fixtures without calling super()."
|
" init_instance_fixtures without calling super()."
|
||||||
@@ -251,7 +251,7 @@ def alias(attr_name):
|
|||||||
|
|
||||||
class WithDefaultDateBounds(object):
|
class WithDefaultDateBounds(object):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin which makes it possible to synchronize date bounds
|
CatalystTestCase mixin which makes it possible to synchronize date bounds
|
||||||
across fixtures.
|
across fixtures.
|
||||||
|
|
||||||
This fixture should always be the last fixture in bases of any fixture or
|
This fixture should always be the last fixture in bases of any fixture or
|
||||||
@@ -264,13 +264,13 @@ class WithDefaultDateBounds(object):
|
|||||||
The date bounds to be used for fixtures that want to have consistent
|
The date bounds to be used for fixtures that want to have consistent
|
||||||
dates.
|
dates.
|
||||||
"""
|
"""
|
||||||
START_DATE = pd.Timestamp('2006-01-03', tz='utc')
|
START_DATE = pd.Timestamp('2016-01-03', tz='utc')
|
||||||
END_DATE = pd.Timestamp('2006-12-29', tz='utc')
|
END_DATE = pd.Timestamp('2016-12-29', tz='utc')
|
||||||
|
|
||||||
|
|
||||||
class WithLogger(object):
|
class WithLogger(object):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.log_handler as an instance-level
|
CatalystTestCase mixin providing cls.log_handler as an instance-level
|
||||||
fixture.
|
fixture.
|
||||||
|
|
||||||
After init_instance_fixtures has been called `self.log_handler` will be a
|
After init_instance_fixtures has been called `self.log_handler` will be a
|
||||||
@@ -295,7 +295,7 @@ class WithLogger(object):
|
|||||||
|
|
||||||
class WithAssetFinder(WithDefaultDateBounds):
|
class WithAssetFinder(WithDefaultDateBounds):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.asset_finder as a class-level fixture.
|
CatalystTestCase mixin providing cls.asset_finder as a class-level fixture.
|
||||||
|
|
||||||
After init_class_fixtures has been called, `cls.asset_finder` is populated
|
After init_class_fixtures has been called, `cls.asset_finder` is populated
|
||||||
with an AssetFinder.
|
with an AssetFinder.
|
||||||
@@ -402,7 +402,7 @@ class WithAssetFinder(WithDefaultDateBounds):
|
|||||||
|
|
||||||
class WithTradingCalendars(object):
|
class WithTradingCalendars(object):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.trading_calendar,
|
CatalystTestCase mixin providing cls.trading_calendar,
|
||||||
cls.all_trading_calendars, cls.trading_calendar_for_asset_type as a
|
cls.all_trading_calendars, cls.trading_calendar_for_asset_type as a
|
||||||
class-level fixture.
|
class-level fixture.
|
||||||
|
|
||||||
@@ -423,7 +423,7 @@ class WithTradingCalendars(object):
|
|||||||
with that asset type.
|
with that asset type.
|
||||||
"""
|
"""
|
||||||
TRADING_CALENDAR_STRS = ('NYSE',)
|
TRADING_CALENDAR_STRS = ('NYSE',)
|
||||||
TRADING_CALENDAR_FOR_ASSET_TYPE = {Equity: 'NYSE', Future: 'us_futures'}
|
TRADING_CALENDAR_FOR_ASSET_TYPE = {Equity: 'NYSE', Future: 'us_futures', }
|
||||||
TRADING_CALENDAR_FOR_EXCHANGE = {}
|
TRADING_CALENDAR_FOR_EXCHANGE = {}
|
||||||
# For backwards compatibility, exisitng tests and fixtures refer to
|
# For backwards compatibility, exisitng tests and fixtures refer to
|
||||||
# `trading_calendar` with the assumption that the value is the NYSE
|
# `trading_calendar` with the assumption that the value is the NYSE
|
||||||
@@ -460,7 +460,7 @@ class WithTradingEnvironment(WithAssetFinder,
|
|||||||
WithTradingCalendars,
|
WithTradingCalendars,
|
||||||
WithDefaultDateBounds):
|
WithDefaultDateBounds):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.env as a class-level fixture.
|
CatalystTestCase mixin providing cls.env as a class-level fixture.
|
||||||
|
|
||||||
After ``init_class_fixtures`` has been called, `cls.env` is populated
|
After ``init_class_fixtures`` has been called, `cls.env` is populated
|
||||||
with a trading environment whose `asset_finder` is the result of
|
with a trading environment whose `asset_finder` is the result of
|
||||||
@@ -560,7 +560,7 @@ class WithTradingEnvironment(WithAssetFinder,
|
|||||||
|
|
||||||
class WithSimParams(WithTradingEnvironment):
|
class WithSimParams(WithTradingEnvironment):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.sim_params as a class level fixture.
|
CatalystTestCase mixin providing cls.sim_params as a class level fixture.
|
||||||
|
|
||||||
The arguments used to construct the trading environment may be overridded
|
The arguments used to construct the trading environment may be overridded
|
||||||
by putting ``SIM_PARAMS_{argname}`` in the class dict except for the
|
by putting ``SIM_PARAMS_{argname}`` in the class dict except for the
|
||||||
@@ -615,7 +615,7 @@ class WithSimParams(WithTradingEnvironment):
|
|||||||
|
|
||||||
class WithTradingSessions(WithTradingCalendars, WithDefaultDateBounds):
|
class WithTradingSessions(WithTradingCalendars, WithDefaultDateBounds):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.trading_days, cls.all_trading_sessions
|
CatalystTestCase mixin providing cls.trading_days, cls.all_trading_sessions
|
||||||
as a class-level fixture.
|
as a class-level fixture.
|
||||||
|
|
||||||
After init_class_fixtures has been called, `cls.all_trading_sessions`
|
After init_class_fixtures has been called, `cls.all_trading_sessions`
|
||||||
@@ -668,7 +668,7 @@ class WithTradingSessions(WithTradingCalendars, WithDefaultDateBounds):
|
|||||||
|
|
||||||
class WithTmpDir(object):
|
class WithTmpDir(object):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixing providing cls.tmpdir as a class-level fixture.
|
CatalystTestCase mixing providing cls.tmpdir as a class-level fixture.
|
||||||
|
|
||||||
After init_class_fixtures has been called, `cls.tmpdir` is populated with
|
After init_class_fixtures has been called, `cls.tmpdir` is populated with
|
||||||
a `testfixtures.TempDirectory` object whose path is `cls.TMP_DIR_PATH`.
|
a `testfixtures.TempDirectory` object whose path is `cls.TMP_DIR_PATH`.
|
||||||
@@ -691,7 +691,7 @@ class WithTmpDir(object):
|
|||||||
|
|
||||||
class WithInstanceTmpDir(object):
|
class WithInstanceTmpDir(object):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixing providing self.tmpdir as an instance-level fixture.
|
CatalystTestCase mixing providing self.tmpdir as an instance-level fixture.
|
||||||
|
|
||||||
After init_instance_fixtures has been called, `self.tmpdir` is populated
|
After init_instance_fixtures has been called, `self.tmpdir` is populated
|
||||||
with a `testfixtures.TempDirectory` object whose path is
|
with a `testfixtures.TempDirectory` object whose path is
|
||||||
@@ -714,7 +714,7 @@ class WithInstanceTmpDir(object):
|
|||||||
|
|
||||||
class WithEquityDailyBarData(WithTradingEnvironment):
|
class WithEquityDailyBarData(WithTradingEnvironment):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.make_equity_daily_bar_data.
|
CatalystTestCase mixin providing cls.make_equity_daily_bar_data.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
@@ -810,7 +810,7 @@ class WithEquityDailyBarData(WithTradingEnvironment):
|
|||||||
|
|
||||||
class WithBcolzEquityDailyBarReader(WithEquityDailyBarData, WithTmpDir):
|
class WithBcolzEquityDailyBarReader(WithEquityDailyBarData, WithTmpDir):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.bcolz_daily_bar_path,
|
CatalystTestCase mixin providing cls.bcolz_daily_bar_path,
|
||||||
cls.bcolz_daily_bar_ctable, and cls.bcolz_equity_daily_bar_reader
|
cls.bcolz_daily_bar_ctable, and cls.bcolz_equity_daily_bar_reader
|
||||||
class level fixtures.
|
class level fixtures.
|
||||||
|
|
||||||
@@ -895,7 +895,7 @@ class WithBcolzEquityDailyBarReader(WithEquityDailyBarData, WithTmpDir):
|
|||||||
|
|
||||||
class WithBcolzEquityDailyBarReaderFromCSVs(WithBcolzEquityDailyBarReader):
|
class WithBcolzEquityDailyBarReaderFromCSVs(WithBcolzEquityDailyBarReader):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin that provides
|
CatalystTestCase mixin that provides
|
||||||
cls.bcolz_equity_daily_bar_reader from a mapping of sids to CSV
|
cls.bcolz_equity_daily_bar_reader from a mapping of sids to CSV
|
||||||
file paths.
|
file paths.
|
||||||
"""
|
"""
|
||||||
@@ -925,7 +925,7 @@ class _WithMinuteBarDataBase(WithTradingEnvironment):
|
|||||||
|
|
||||||
class WithEquityMinuteBarData(_WithMinuteBarDataBase):
|
class WithEquityMinuteBarData(_WithMinuteBarDataBase):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.equity_minute_bar_days.
|
CatalystTestCase mixin providing cls.equity_minute_bar_days.
|
||||||
|
|
||||||
After init_class_fixtures has been called:
|
After init_class_fixtures has been called:
|
||||||
- `cls.equity_minute_bar_days` has the range over which data has been
|
- `cls.equity_minute_bar_days` has the range over which data has been
|
||||||
@@ -984,7 +984,7 @@ class WithEquityMinuteBarData(_WithMinuteBarDataBase):
|
|||||||
|
|
||||||
class WithFutureMinuteBarData(_WithMinuteBarDataBase):
|
class WithFutureMinuteBarData(_WithMinuteBarDataBase):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.future_minute_bar_days.
|
CatalystTestCase mixin providing cls.future_minute_bar_days.
|
||||||
|
|
||||||
After init_class_fixtures has been called:
|
After init_class_fixtures has been called:
|
||||||
- `cls.future_minute_bar_days` has the range over which data has been
|
- `cls.future_minute_bar_days` has the range over which data has been
|
||||||
@@ -1044,7 +1044,7 @@ class WithFutureMinuteBarData(_WithMinuteBarDataBase):
|
|||||||
|
|
||||||
class WithBcolzEquityMinuteBarReader(WithEquityMinuteBarData, WithTmpDir):
|
class WithBcolzEquityMinuteBarReader(WithEquityMinuteBarData, WithTmpDir):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.bcolz_minute_bar_path,
|
CatalystTestCase mixin providing cls.bcolz_minute_bar_path,
|
||||||
cls.bcolz_minute_bar_ctable, and cls.bcolz_equity_minute_bar_reader
|
cls.bcolz_minute_bar_ctable, and cls.bcolz_equity_minute_bar_reader
|
||||||
class level fixtures.
|
class level fixtures.
|
||||||
|
|
||||||
@@ -1103,7 +1103,7 @@ class WithBcolzEquityMinuteBarReader(WithEquityMinuteBarData, WithTmpDir):
|
|||||||
|
|
||||||
class WithBcolzFutureMinuteBarReader(WithFutureMinuteBarData, WithTmpDir):
|
class WithBcolzFutureMinuteBarReader(WithFutureMinuteBarData, WithTmpDir):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.bcolz_minute_bar_path,
|
CatalystTestCase mixin providing cls.bcolz_minute_bar_path,
|
||||||
cls.bcolz_minute_bar_ctable, and cls.bcolz_equity_minute_bar_reader
|
cls.bcolz_minute_bar_ctable, and cls.bcolz_equity_minute_bar_reader
|
||||||
class level fixtures.
|
class level fixtures.
|
||||||
|
|
||||||
@@ -1227,7 +1227,7 @@ class WithConstantFutureMinuteBarData(WithFutureMinuteBarData):
|
|||||||
|
|
||||||
class WithAdjustmentReader(WithBcolzEquityDailyBarReader):
|
class WithAdjustmentReader(WithBcolzEquityDailyBarReader):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing cls.adjustment_reader as a class level
|
CatalystTestCase mixin providing cls.adjustment_reader as a class level
|
||||||
fixture.
|
fixture.
|
||||||
|
|
||||||
After init_class_fixtures has been called, `cls.adjustment_reader` will be
|
After init_class_fixtures has been called, `cls.adjustment_reader` will be
|
||||||
@@ -1359,7 +1359,7 @@ class WithEquityPricingPipelineEngine(WithAdjustmentReader,
|
|||||||
|
|
||||||
class WithSeededRandomPipelineEngine(WithTradingSessions, WithAssetFinder):
|
class WithSeededRandomPipelineEngine(WithTradingSessions, WithAssetFinder):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing class-level fixtures for running pipelines
|
CatalystTestCase mixin providing class-level fixtures for running pipelines
|
||||||
against deterministically-generated random data.
|
against deterministically-generated random data.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
@@ -1434,7 +1434,7 @@ class WithDataPortal(WithAdjustmentReader,
|
|||||||
WithBcolzEquityMinuteBarReader,
|
WithBcolzEquityMinuteBarReader,
|
||||||
WithBcolzFutureMinuteBarReader):
|
WithBcolzFutureMinuteBarReader):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin providing self.data_portal as an instance level
|
CatalystTestCase mixin providing self.data_portal as an instance level
|
||||||
fixture.
|
fixture.
|
||||||
|
|
||||||
After init_instance_fixtures has been called, `self.data_portal` will be
|
After init_instance_fixtures has been called, `self.data_portal` will be
|
||||||
@@ -1485,12 +1485,12 @@ class WithDataPortal(WithAdjustmentReader,
|
|||||||
self.env.asset_finder,
|
self.env.asset_finder,
|
||||||
self.trading_calendar,
|
self.trading_calendar,
|
||||||
first_trading_day=self.DATA_PORTAL_FIRST_TRADING_DAY,
|
first_trading_day=self.DATA_PORTAL_FIRST_TRADING_DAY,
|
||||||
equity_daily_reader=(
|
daily_reader=(
|
||||||
self.bcolz_equity_daily_bar_reader
|
self.bcolz_equity_daily_bar_reader
|
||||||
if self.DATA_PORTAL_USE_DAILY_DATA else
|
if self.DATA_PORTAL_USE_DAILY_DATA else
|
||||||
None
|
None
|
||||||
),
|
),
|
||||||
equity_minute_reader=(
|
minute_reader=(
|
||||||
self.bcolz_equity_minute_bar_reader
|
self.bcolz_equity_minute_bar_reader
|
||||||
if self.DATA_PORTAL_USE_MINUTE_DATA else
|
if self.DATA_PORTAL_USE_MINUTE_DATA else
|
||||||
None
|
None
|
||||||
@@ -1526,7 +1526,7 @@ class WithDataPortal(WithAdjustmentReader,
|
|||||||
|
|
||||||
class WithResponses(object):
|
class WithResponses(object):
|
||||||
"""
|
"""
|
||||||
ZiplineTestCase mixin that provides self.responses as an instance
|
CatalystTestCase mixin that provides self.responses as an instance
|
||||||
fixture.
|
fixture.
|
||||||
|
|
||||||
After init_instance_fixtures has been called, `self.responses` will be
|
After init_instance_fixtures has been called, `self.responses` will be
|
||||||
|
|||||||
@@ -31,5 +31,4 @@ class OpenExchangeCalendar(TradingCalendar):
|
|||||||
return DateOffset(days=1)
|
return DateOffset(days=1)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(OpenExchangeCalendar, self).__init__(
|
super(OpenExchangeCalendar, self).__init__(start=Timestamp('2015-3-1', tz='UTC'), **kwargs)
|
||||||
start=Timestamp('2015-3-1', tz='UTC'), **kwargs)
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ DEFAULT_BAR_TEMPLATE = ' [%(bar)s] %(label)s: %(info)s'
|
|||||||
DEFAULT_EMPTY_CHAR = ' '
|
DEFAULT_EMPTY_CHAR = ' '
|
||||||
DEFAULT_FILL_CHAR = '='
|
DEFAULT_FILL_CHAR = '='
|
||||||
|
|
||||||
|
|
||||||
def item_show_count(total=None):
|
def item_show_count(total=None):
|
||||||
def maybe_show_total(index):
|
def maybe_show_total(index):
|
||||||
if total is not None:
|
if total is not None:
|
||||||
@@ -18,13 +17,12 @@ def item_show_count(total=None):
|
|||||||
|
|
||||||
def item_show_func(item, _it=iter(count())):
|
def item_show_func(item, _it=iter(count())):
|
||||||
if item is not None:
|
if item is not None:
|
||||||
# starting = False
|
starting = False
|
||||||
return maybe_show_total(next(_it))
|
return maybe_show_total(next(_it))
|
||||||
return 'DONE'
|
return 'DONE'
|
||||||
|
|
||||||
return item_show_func
|
return item_show_func
|
||||||
|
|
||||||
|
|
||||||
def maybe_show_progress(it,
|
def maybe_show_progress(it,
|
||||||
show_progress,
|
show_progress,
|
||||||
empty_char=DEFAULT_EMPTY_CHAR,
|
empty_char=DEFAULT_EMPTY_CHAR,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from catalyst.utils.input_validation import expect_types
|
|||||||
__all__ = ['load_from_yahoo', 'load_bars_from_yahoo']
|
__all__ = ['load_from_yahoo', 'load_bars_from_yahoo']
|
||||||
|
|
||||||
|
|
||||||
def create_simulation_parameters(year=2006, start=None, end=None,
|
def create_simulation_parameters(year=2016, start=None, end=None,
|
||||||
capital_base=float("1.0e5"),
|
capital_base=float("1.0e5"),
|
||||||
num_days=None,
|
num_days=None,
|
||||||
data_frequency='daily',
|
data_frequency='daily',
|
||||||
|
|||||||
@@ -17,11 +17,9 @@ import math
|
|||||||
|
|
||||||
from numpy import isnan
|
from numpy import isnan
|
||||||
|
|
||||||
|
|
||||||
def round_nearest(x, a):
|
def round_nearest(x, a):
|
||||||
return round(round(x / a) * a, -int(math.floor(math.log10(a))))
|
return round(round(x / a) * a, -int(math.floor(math.log10(a))))
|
||||||
|
|
||||||
|
|
||||||
def tolerant_equals(a, b, atol=10e-7, rtol=10e-7, equal_nan=False):
|
def tolerant_equals(a, b, atol=10e-7, rtol=10e-7, equal_nan=False):
|
||||||
"""Check if a and b are equal with some tolerance.
|
"""Check if a and b are equal with some tolerance.
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ def catalyst_root(environ=None):
|
|||||||
|
|
||||||
root = environ.get('ZIPLINE_ROOT', None)
|
root = environ.get('ZIPLINE_ROOT', None)
|
||||||
if root is None:
|
if root is None:
|
||||||
root = os.path.join(expanduser('~'), '.catalyst')
|
root = os.path.join(expanduser('~'),'.catalyst')
|
||||||
|
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|||||||
+232
-285
@@ -8,13 +8,10 @@ from time import sleep
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.data.bundles import load
|
from catalyst.data.bundles import load
|
||||||
from catalyst.data.data_portal import DataPortal
|
from catalyst.data.data_portal import DataPortal
|
||||||
from catalyst.exchange.exchange_pricing_loader import ExchangePricingLoader, \
|
from catalyst.exchange.factory import get_exchange
|
||||||
TradingPairPricing
|
|
||||||
from catalyst.exchange.utils.factory import get_exchange
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pygments import highlight
|
from pygments import highlight
|
||||||
@@ -33,16 +30,17 @@ from catalyst.utils.factory import create_simulation_parameters
|
|||||||
from catalyst.data.loader import load_crypto_market_data
|
from catalyst.data.loader import load_crypto_market_data
|
||||||
import catalyst.utils.paths as pth
|
import catalyst.utils.paths as pth
|
||||||
|
|
||||||
from catalyst.exchange.exchange_algorithm import (
|
from catalyst.exchange.exchange_algorithm import ExchangeTradingAlgorithmLive, \
|
||||||
ExchangeTradingAlgorithmLive,
|
ExchangeTradingAlgorithmBacktest
|
||||||
ExchangeTradingAlgorithmBacktest,
|
|
||||||
)
|
|
||||||
from catalyst.exchange.exchange_data_portal import DataPortalExchangeLive, \
|
from catalyst.exchange.exchange_data_portal import DataPortalExchangeLive, \
|
||||||
DataPortalExchangeBacktest
|
DataPortalExchangeBacktest
|
||||||
from catalyst.exchange.exchange_asset_finder import ExchangeAssetFinder
|
from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
|
||||||
|
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
||||||
from catalyst.exchange.exchange_errors import (
|
from catalyst.exchange.exchange_errors import (
|
||||||
ExchangeRequestError, ExchangeRequestErrorTooManyAttempts,
|
ExchangeRequestError, ExchangeRequestErrorTooManyAttempts,
|
||||||
BaseCurrencyNotFoundError, NotEnoughCapitalError)
|
BaseCurrencyNotFoundError)
|
||||||
|
from catalyst.exchange.exchange_utils import get_algo_object
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
@@ -70,7 +68,34 @@ class _RunAlgoError(click.ClickException, ValueError):
|
|||||||
return self.pyfunc_msg
|
return self.pyfunc_msg
|
||||||
|
|
||||||
|
|
||||||
def _build_namespace(algotext, local_namespace, defines):
|
def _run(handle_data,
|
||||||
|
initialize,
|
||||||
|
before_trading_start,
|
||||||
|
analyze,
|
||||||
|
algofile,
|
||||||
|
algotext,
|
||||||
|
defines,
|
||||||
|
data_frequency,
|
||||||
|
capital_base,
|
||||||
|
data,
|
||||||
|
bundle,
|
||||||
|
bundle_timestamp,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
output,
|
||||||
|
print_algo,
|
||||||
|
local_namespace,
|
||||||
|
environ,
|
||||||
|
live,
|
||||||
|
exchange,
|
||||||
|
algo_namespace,
|
||||||
|
base_currency,
|
||||||
|
live_graph,
|
||||||
|
simulate_orders):
|
||||||
|
"""Run a backtest for the given algorithm.
|
||||||
|
|
||||||
|
This is shared between the cli and :func:`catalyst.run_algo`.
|
||||||
|
"""
|
||||||
if algotext is not None:
|
if algotext is not None:
|
||||||
if local_namespace:
|
if local_namespace:
|
||||||
ip = get_ipython() # noqa
|
ip = get_ipython() # noqa
|
||||||
@@ -84,197 +109,179 @@ def _build_namespace(algotext, local_namespace, defines):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'invalid define %r, should be of the form name=value' %
|
'invalid define %r, should be of the form name=value' %
|
||||||
assign)
|
assign,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
# evaluate in the same namespace so names may refer to
|
# evaluate in the same namespace so names may refer to
|
||||||
# eachother
|
# eachother
|
||||||
namespace[name] = eval(value, namespace)
|
namespace[name] = eval(value, namespace)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'failed to execute definition for name %r: %s' % (name, e))
|
'failed to execute definition for name %r: %s' % (name, e),
|
||||||
|
)
|
||||||
elif defines:
|
elif defines:
|
||||||
raise _RunAlgoError(
|
raise _RunAlgoError(
|
||||||
'cannot pass define without `algotext`',
|
'cannot pass define without `algotext`',
|
||||||
"cannot pass '-D' / '--define' without '-t' / '--algotext'")
|
"cannot pass '-D' / '--define' without '-t' / '--algotext'",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
namespace = {}
|
namespace = {}
|
||||||
|
if algofile is not None:
|
||||||
|
algotext = algofile.read()
|
||||||
|
|
||||||
return namespace
|
if print_algo:
|
||||||
|
if PYGMENTS:
|
||||||
|
highlight(
|
||||||
|
algotext,
|
||||||
|
PythonLexer(),
|
||||||
|
TerminalFormatter(),
|
||||||
|
outfile=sys.stdout,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
click.echo(algotext)
|
||||||
|
|
||||||
|
mode = 'live' if live else 'backtest'
|
||||||
|
log.info('running algo in {mode} mode'.format(mode=mode))
|
||||||
|
|
||||||
def _mode(simulate_orders, live):
|
|
||||||
if not live:
|
|
||||||
return 'backtest'
|
|
||||||
elif simulate_orders:
|
|
||||||
return 'paper-trading'
|
|
||||||
else:
|
|
||||||
return 'live-trading'
|
|
||||||
|
|
||||||
|
|
||||||
def _build_exchanges_dict(exchange, live, simulate_orders, base_currency):
|
|
||||||
exchange_name = exchange
|
exchange_name = exchange
|
||||||
if exchange_name is None:
|
if exchange_name is None:
|
||||||
raise ValueError('Please specify at least one exchange.')
|
raise ValueError('Please specify at least one exchange.')
|
||||||
|
|
||||||
exchange_list = [x.strip().lower() for x in exchange.split(',')]
|
exchange_list = [x.strip().lower() for x in exchange.split(',')]
|
||||||
|
|
||||||
exchanges = {exchange_name: get_exchange(
|
exchanges = dict()
|
||||||
exchange_name=exchange_name,
|
for exchange_name in exchange_list:
|
||||||
base_currency=base_currency,
|
|
||||||
must_authenticate=(live and not simulate_orders))
|
|
||||||
for exchange_name in exchange_list}
|
|
||||||
|
|
||||||
return exchanges
|
# Looking for the portfolio from the cache first
|
||||||
|
portfolio = get_algo_object(
|
||||||
|
algo_name=algo_namespace,
|
||||||
|
key='portfolio_{}'.format(exchange_name),
|
||||||
|
environ=environ
|
||||||
|
)
|
||||||
|
|
||||||
|
if portfolio is None:
|
||||||
|
portfolio = ExchangePortfolio(
|
||||||
|
start if start is not None else pd.Timestamp.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
def _pretty_print_code(algotext):
|
exchanges[exchange_name] = get_exchange(
|
||||||
if PYGMENTS:
|
exchange_name=exchange_name,
|
||||||
highlight(
|
|
||||||
algotext,
|
|
||||||
PythonLexer(),
|
|
||||||
TerminalFormatter(),
|
|
||||||
outfile=sys.stdout)
|
|
||||||
else:
|
|
||||||
click.echo(algotext)
|
|
||||||
|
|
||||||
|
|
||||||
def _choose_loader(data_frequency, column):
|
|
||||||
bound_cols = TradingPairPricing.columns
|
|
||||||
if column in bound_cols:
|
|
||||||
return ExchangePricingLoader(data_frequency)
|
|
||||||
raise ValueError(
|
|
||||||
"No PipelineLoader registered for column %s." % column)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_live_time_range():
|
|
||||||
start = pd.Timestamp.utcnow()
|
|
||||||
# TODO: fix the end data.
|
|
||||||
end = start + timedelta(hours=8760)
|
|
||||||
return start, end
|
|
||||||
|
|
||||||
|
|
||||||
def _data_for_live_trading(sim_params, exchanges, env, open_calendar):
|
|
||||||
data = DataPortalExchangeLive(
|
|
||||||
exchanges=exchanges,
|
|
||||||
asset_finder=env.asset_finder,
|
|
||||||
trading_calendar=open_calendar,
|
|
||||||
first_trading_day=pd.to_datetime('today', utc=True))
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
# TODO use proper retry here
|
|
||||||
def _fetch_capital_base(base_currency, exchange_name, exchange,
|
|
||||||
attempt_index=0):
|
|
||||||
"""
|
|
||||||
Fetch the base currency amount required to bootstrap
|
|
||||||
the algorithm against the exchange.
|
|
||||||
|
|
||||||
The algorithm cannot continue without this value.
|
|
||||||
|
|
||||||
:param exchange: the targeted exchange
|
|
||||||
:param attempt_index:
|
|
||||||
:return capital_base: the amount of base currency available for
|
|
||||||
trading
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
log.debug('retrieving capital base in {} to bootstrap '
|
|
||||||
'exchange {}'.format(base_currency, exchange_name))
|
|
||||||
balances = exchange.get_balances()
|
|
||||||
except ExchangeRequestError as e:
|
|
||||||
if attempt_index < 20:
|
|
||||||
log.warn(
|
|
||||||
'could not retrieve balances on {}: {}'.format(
|
|
||||||
exchange.name, e))
|
|
||||||
sleep(5)
|
|
||||||
return _fetch_capital_base(base_currency, exchange_name, exchange,
|
|
||||||
attempt_index + 1)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ExchangeRequestErrorTooManyAttempts(
|
|
||||||
attempts=attempt_index,
|
|
||||||
error=e)
|
|
||||||
|
|
||||||
if base_currency in balances:
|
|
||||||
base_currency_available = balances[base_currency]['free']
|
|
||||||
log.info(
|
|
||||||
'base currency available in the account: {} {}'.format(
|
|
||||||
base_currency_available, base_currency))
|
|
||||||
|
|
||||||
return base_currency_available
|
|
||||||
else:
|
|
||||||
raise BaseCurrencyNotFoundError(
|
|
||||||
base_currency=base_currency,
|
base_currency=base_currency,
|
||||||
exchange=exchange_name)
|
portfolio=portfolio,
|
||||||
|
must_authenticate=live,
|
||||||
|
)
|
||||||
|
|
||||||
|
open_calendar = get_calendar('OPEN')
|
||||||
|
|
||||||
def _algorithm_class_for_live(algo_namespace, live_graph, stats_output,
|
env = TradingEnvironment(
|
||||||
analyze_live, base_currency, simulate_orders,
|
load=partial(
|
||||||
exchanges, capital_base):
|
load_crypto_market_data,
|
||||||
if not simulate_orders:
|
environ=environ,
|
||||||
|
start_dt=start,
|
||||||
|
end_dt=end
|
||||||
|
),
|
||||||
|
environ=environ,
|
||||||
|
exchange_tz='UTC',
|
||||||
|
asset_db_path=None # We don't need an asset db, we have exchanges
|
||||||
|
)
|
||||||
|
env.asset_finder = AssetFinderExchange()
|
||||||
|
choose_loader = None # TODO: use the DataPortal for in the algorithm class for this
|
||||||
|
|
||||||
|
if live:
|
||||||
|
start = pd.Timestamp.utcnow()
|
||||||
|
|
||||||
|
# TODO: fix the end data.
|
||||||
|
end = start + timedelta(hours=8760)
|
||||||
|
|
||||||
|
data = DataPortalExchangeLive(
|
||||||
|
exchanges=exchanges,
|
||||||
|
asset_finder=env.asset_finder,
|
||||||
|
trading_calendar=open_calendar,
|
||||||
|
first_trading_day=pd.to_datetime('today', utc=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
def fetch_capital_base(exchange, attempt_index=0):
|
||||||
|
"""
|
||||||
|
Fetch the base currency amount required to bootstrap
|
||||||
|
the algorithm against the exchange.
|
||||||
|
|
||||||
|
The algorithm cannot continue without this value.
|
||||||
|
|
||||||
|
:param exchange: the targeted exchange
|
||||||
|
:param attempt_index:
|
||||||
|
:return capital_base: the amount of base currency available for
|
||||||
|
trading
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
log.debug('retrieving capital base in {} to bootstrap '
|
||||||
|
'exchange {}'.format(base_currency, exchange_name))
|
||||||
|
balances = exchange.get_balances()
|
||||||
|
except ExchangeRequestError as e:
|
||||||
|
if attempt_index < 20:
|
||||||
|
log.warn(
|
||||||
|
'could not retrieve balances on {}: {}'.format(
|
||||||
|
exchange.name, e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sleep(5)
|
||||||
|
return fetch_capital_base(exchange, attempt_index + 1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ExchangeRequestErrorTooManyAttempts(
|
||||||
|
attempts=attempt_index,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
|
if base_currency in balances:
|
||||||
|
base_currency_available = balances[base_currency]['free']
|
||||||
|
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:
|
||||||
|
raise BaseCurrencyNotFoundError(
|
||||||
|
base_currency=base_currency,
|
||||||
|
exchange=exchange_name
|
||||||
|
)
|
||||||
|
|
||||||
|
combined_capital_base = 0
|
||||||
for exchange_name in exchanges:
|
for exchange_name in exchanges:
|
||||||
exchange = exchanges[exchange_name]
|
exchange = exchanges[exchange_name]
|
||||||
balance = _fetch_capital_base(base_currency, exchange_name,
|
combined_capital_base += fetch_capital_base(exchange)
|
||||||
exchange)
|
|
||||||
|
|
||||||
if balance < capital_base:
|
sim_params = create_simulation_parameters(
|
||||||
raise NotEnoughCapitalError(
|
start=start,
|
||||||
exchange=exchange_name,
|
end=end,
|
||||||
base_currency=base_currency,
|
capital_base=capital_base,
|
||||||
balance=balance,
|
emission_rate='minute',
|
||||||
capital_base=capital_base)
|
data_frequency='minute'
|
||||||
|
)
|
||||||
|
|
||||||
algorithm_class = partial(
|
# TODO: use the constructor instead
|
||||||
ExchangeTradingAlgorithmLive,
|
sim_params._arena = 'live'
|
||||||
exchanges=exchanges,
|
|
||||||
algo_namespace=algo_namespace,
|
|
||||||
live_graph=live_graph,
|
|
||||||
simulate_orders=simulate_orders,
|
|
||||||
stats_output=stats_output,
|
|
||||||
analyze_live=analyze_live,)
|
|
||||||
|
|
||||||
return algorithm_class
|
algorithm_class = partial(
|
||||||
|
ExchangeTradingAlgorithmLive,
|
||||||
|
exchanges=exchanges,
|
||||||
def _bundle_trading_environment(bundle_data, environ):
|
algo_namespace=algo_namespace,
|
||||||
prefix, connstr = re.split(
|
live_graph=live_graph,
|
||||||
r'sqlite:///',
|
simulate_orders=simulate_orders
|
||||||
str(bundle_data.asset_finder.engine.url),
|
)
|
||||||
maxsplit=1)
|
elif exchanges:
|
||||||
if prefix:
|
|
||||||
raise ValueError(
|
|
||||||
"invalid url %r, must begin with 'sqlite:///'" %
|
|
||||||
str(bundle_data.asset_finder.engine.url))
|
|
||||||
|
|
||||||
return TradingEnvironment(asset_db_path=connstr, environ=environ)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_live_algo_and_data(sim_params, exchanges, env, open_calendar,
|
|
||||||
simulate_orders, algo_namespace, capital_base,
|
|
||||||
live_graph, stats_output, analyze_live,
|
|
||||||
base_currency, namespace, choose_loader,
|
|
||||||
algorithm_class_kwargs):
|
|
||||||
sim_params._arena = 'live' # TODO: use the constructor instead
|
|
||||||
|
|
||||||
data = _data_for_live_trading(sim_params, exchanges, env, open_calendar)
|
|
||||||
|
|
||||||
algorithm_class = _algorithm_class_for_live(
|
|
||||||
algo_namespace, live_graph, stats_output, analyze_live,
|
|
||||||
base_currency, simulate_orders, exchanges, capital_base)
|
|
||||||
|
|
||||||
return data, algorithm_class(
|
|
||||||
namespace=namespace,
|
|
||||||
env=env,
|
|
||||||
get_pipeline_loader=choose_loader,
|
|
||||||
sim_params=sim_params,
|
|
||||||
**algorithm_class_kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_backtest_algo_and_data(
|
|
||||||
exchanges, bundle, env, environ, bundle_timestamp, open_calendar,
|
|
||||||
start, end, namespace, choose_loader, sim_params,
|
|
||||||
algorithm_class_kwargs):
|
|
||||||
if 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.
|
||||||
|
|
||||||
@@ -288,19 +295,41 @@ def _build_backtest_algo_and_data(
|
|||||||
asset_finder=None,
|
asset_finder=None,
|
||||||
trading_calendar=open_calendar,
|
trading_calendar=open_calendar,
|
||||||
first_trading_day=start,
|
first_trading_day=start,
|
||||||
last_available_session=end)
|
last_available_session=end
|
||||||
|
)
|
||||||
|
|
||||||
|
sim_params = create_simulation_parameters(
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
capital_base=capital_base,
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
emission_rate=data_frequency,
|
||||||
|
)
|
||||||
|
|
||||||
algorithm_class = partial(
|
algorithm_class = partial(
|
||||||
ExchangeTradingAlgorithmBacktest,
|
ExchangeTradingAlgorithmBacktest,
|
||||||
exchanges=exchanges)
|
exchanges=exchanges
|
||||||
|
)
|
||||||
|
|
||||||
elif bundle is not None:
|
elif bundle is not None:
|
||||||
# TODO This branch should probably be removed or fixed: it doesn't even
|
bundle_data = load(
|
||||||
# build `algorithm_class`, so it will break when trying to instantiate
|
bundle,
|
||||||
# it.
|
environ,
|
||||||
bundle_data = load(bundle, environ, bundle_timestamp)
|
bundle_timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
env = _bundle_trading_environment(bundle_data, environ)
|
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 = \
|
first_trading_day = \
|
||||||
bundle_data.equity_minute_bar_reader.first_trading_day
|
bundle_data.equity_minute_bar_reader.first_trading_day
|
||||||
|
|
||||||
@@ -309,103 +338,27 @@ def _build_backtest_algo_and_data(
|
|||||||
first_trading_day=first_trading_day,
|
first_trading_day=first_trading_day,
|
||||||
equity_minute_reader=bundle_data.equity_minute_bar_reader,
|
equity_minute_reader=bundle_data.equity_minute_bar_reader,
|
||||||
equity_daily_reader=bundle_data.equity_daily_bar_reader,
|
equity_daily_reader=bundle_data.equity_daily_bar_reader,
|
||||||
adjustment_reader=bundle_data.adjustment_reader)
|
adjustment_reader=bundle_data.adjustment_reader,
|
||||||
|
)
|
||||||
|
|
||||||
return data, algorithm_class(
|
perf = algorithm_class(
|
||||||
namespace=namespace,
|
namespace=namespace,
|
||||||
env=env,
|
env=env,
|
||||||
get_pipeline_loader=choose_loader,
|
get_pipeline_loader=choose_loader,
|
||||||
sim_params=sim_params,
|
sim_params=sim_params,
|
||||||
**algorithm_class_kwargs)
|
**{
|
||||||
|
'initialize': initialize,
|
||||||
|
'handle_data': handle_data,
|
||||||
def _build_algo_and_data(handle_data, initialize, before_trading_start,
|
'before_trading_start': before_trading_start,
|
||||||
analyze, algofile, algotext, defines, data_frequency,
|
'analyze': analyze,
|
||||||
capital_base, data, bundle, bundle_timestamp, start,
|
} if algotext is None else {
|
||||||
end, output, print_algo, local_namespace, environ,
|
'algo_filename': getattr(algofile, 'name', '<algorithm>'),
|
||||||
live, exchange, algo_namespace, base_currency,
|
'script': algotext,
|
||||||
live_graph, analyze_live, simulate_orders,
|
}
|
||||||
stats_output):
|
).run(
|
||||||
namespace = _build_namespace(algotext, local_namespace, defines)
|
|
||||||
if algotext is not None:
|
|
||||||
algotext = algofile.read()
|
|
||||||
|
|
||||||
if print_algo:
|
|
||||||
_pretty_print_code(algotext)
|
|
||||||
|
|
||||||
mode = _mode(simulate_orders, live)
|
|
||||||
log.info('running algo in {mode} mode'.format(mode=mode))
|
|
||||||
|
|
||||||
exchanges = _build_exchanges_dict(exchange, live, simulate_orders,
|
|
||||||
base_currency)
|
|
||||||
|
|
||||||
open_calendar = get_calendar('OPEN')
|
|
||||||
|
|
||||||
env = TradingEnvironment(
|
|
||||||
load=partial(load_crypto_market_data, environ=environ, start_dt=start,
|
|
||||||
end_dt=end),
|
|
||||||
environ=environ,
|
|
||||||
exchange_tz='UTC',
|
|
||||||
asset_db_path=None) # We don't need an asset db, we have exchanges
|
|
||||||
|
|
||||||
env.asset_finder = ExchangeAssetFinder(exchanges=exchanges)
|
|
||||||
|
|
||||||
choose_loader = partial(_choose_loader, data_frequency)
|
|
||||||
|
|
||||||
if live:
|
|
||||||
start, end = _get_live_time_range()
|
|
||||||
data_frequency = 'minute' # TODO double check if this is the desired behavior
|
|
||||||
|
|
||||||
sim_params = create_simulation_parameters(
|
|
||||||
start=start,
|
|
||||||
end=end,
|
|
||||||
capital_base=capital_base,
|
|
||||||
emission_rate=data_frequency,
|
|
||||||
data_frequency=data_frequency)
|
|
||||||
|
|
||||||
if algotext is None:
|
|
||||||
algorithm_class_kwargs = {'initialize': initialize,
|
|
||||||
'handle_data': handle_data,
|
|
||||||
'before_trading_start': before_trading_start,
|
|
||||||
'analyze': analyze}
|
|
||||||
else:
|
|
||||||
algorithm_class_kwargs = {'algo_filename': getattr(algofile, 'name',
|
|
||||||
'<algorithm>'),
|
|
||||||
'script': algotext}
|
|
||||||
|
|
||||||
if live:
|
|
||||||
return _build_live_algo_and_data(
|
|
||||||
sim_params, exchanges, env, open_calendar, simulate_orders,
|
|
||||||
algo_namespace, capital_base, live_graph, stats_output,
|
|
||||||
analyze_live, base_currency, namespace, choose_loader,
|
|
||||||
algorithm_class_kwargs)
|
|
||||||
else:
|
|
||||||
return _build_backtest_algo_and_data(
|
|
||||||
exchanges, bundle, env, environ, bundle_timestamp, open_calendar,
|
|
||||||
start, end, namespace, choose_loader, sim_params,
|
|
||||||
algorithm_class_kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def _run(handle_data, initialize, before_trading_start, analyze, algofile,
|
|
||||||
algotext, defines, data_frequency, capital_base, data, bundle,
|
|
||||||
bundle_timestamp, start, end, output, print_algo, local_namespace,
|
|
||||||
environ, live, exchange, algo_namespace, base_currency, live_graph,
|
|
||||||
analyze_live, simulate_orders, stats_output):
|
|
||||||
"""Run an algorithm in backtest,
|
|
||||||
paper-trading or live-trading mode.
|
|
||||||
|
|
||||||
This is shared between the cli and :func:`catalyst.run_algo`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
data, algorithm = _build_algo_and_data(
|
|
||||||
handle_data, initialize, before_trading_start, analyze, algofile,
|
|
||||||
algotext, defines, data_frequency, capital_base, data, bundle,
|
|
||||||
bundle_timestamp, start, end, output, print_algo, local_namespace,
|
|
||||||
environ, live, exchange, algo_namespace, base_currency, live_graph,
|
|
||||||
analyze_live, simulate_orders, stats_output)
|
|
||||||
perf = algorithm.run(
|
|
||||||
data,
|
data,
|
||||||
overwrite_sim_params=False)
|
overwrite_sim_params=False,
|
||||||
|
)
|
||||||
|
|
||||||
if output == '-':
|
if output == '-':
|
||||||
click.echo(str(perf))
|
click.echo(str(perf))
|
||||||
@@ -462,7 +415,8 @@ def load_extensions(default, extensions, strict, environ, reload=False):
|
|||||||
# without `strict` we should just log the failure
|
# without `strict` we should just log the failure
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
'Failed to load extension: %r\n%s' % (ext, e),
|
'Failed to load extension: %r\n%s' % (ext, e),
|
||||||
stacklevel=2)
|
stacklevel=2
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
_loaded_extensions.add(ext)
|
_loaded_extensions.add(ext)
|
||||||
|
|
||||||
@@ -487,9 +441,7 @@ def run_algorithm(initialize,
|
|||||||
base_currency=None,
|
base_currency=None,
|
||||||
algo_namespace=None,
|
algo_namespace=None,
|
||||||
live_graph=False,
|
live_graph=False,
|
||||||
analyze_live=None,
|
|
||||||
simulate_orders=True,
|
simulate_orders=True,
|
||||||
stats_output=None,
|
|
||||||
output=os.devnull):
|
output=os.devnull):
|
||||||
"""Run a trading algorithm.
|
"""Run a trading algorithm.
|
||||||
|
|
||||||
@@ -561,15 +513,9 @@ def run_algorithm(initialize,
|
|||||||
catalyst.data.bundles.bundles : The available data bundles.
|
catalyst.data.bundles.bundles : The available data bundles.
|
||||||
"""
|
"""
|
||||||
load_extensions(
|
load_extensions(
|
||||||
default_extension, extensions, strict_extensions, environ)
|
default_extension, extensions, strict_extensions, environ
|
||||||
|
)
|
||||||
|
|
||||||
if capital_base is None:
|
|
||||||
raise ValueError(
|
|
||||||
'Please specify a `capital_base` parameter which is the maximum '
|
|
||||||
'amount of base currency available for trading. For example, '
|
|
||||||
'if the `capital_base` is 5ETH, the '
|
|
||||||
'`order_target_percent(asset, 1)` command will order 5ETH worth '
|
|
||||||
'of the specified asset.')
|
|
||||||
# I'm not sure that we need this since the modified DataPortal
|
# 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.
|
||||||
|
|
||||||
@@ -587,11 +533,13 @@ def run_algorithm(initialize,
|
|||||||
elif len(non_none_data) != 1:
|
elif len(non_none_data) != 1:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'must specify one of `data`, `data_portal`, or `bundle`,'
|
'must specify one of `data`, `data_portal`, or `bundle`,'
|
||||||
' got: %r' % non_none_data)
|
' got: %r' % non_none_data,
|
||||||
|
)
|
||||||
|
|
||||||
elif 'bundle' not in non_none_data and bundle_timestamp is not None:
|
elif 'bundle' not in non_none_data and bundle_timestamp is not None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'cannot specify `bundle_timestamp` without passing `bundle`')
|
'cannot specify `bundle_timestamp` without passing `bundle`',
|
||||||
|
)
|
||||||
return _run(
|
return _run(
|
||||||
handle_data=handle_data,
|
handle_data=handle_data,
|
||||||
initialize=initialize,
|
initialize=initialize,
|
||||||
@@ -616,6 +564,5 @@ def run_algorithm(initialize,
|
|||||||
algo_namespace=algo_namespace,
|
algo_namespace=algo_namespace,
|
||||||
base_currency=base_currency,
|
base_currency=base_currency,
|
||||||
live_graph=live_graph,
|
live_graph=live_graph,
|
||||||
analyze_live=analyze_live,
|
simulate_orders=simulate_orders
|
||||||
simulate_orders=simulate_orders,
|
)
|
||||||
stats_output=stats_output)
|
|
||||||
|
|||||||
+2
-15804
File diff suppressed because it is too large
Load Diff
@@ -31,11 +31,6 @@ Overview
|
|||||||
`two-part video tutorial <videos.html#backtesting-a-strategy>`_ to show how
|
`two-part video tutorial <videos.html#backtesting-a-strategy>`_ to show how
|
||||||
to get started in backtesting and live trading with Catalyst.
|
to get started in backtesting and live trading with Catalyst.
|
||||||
|
|
||||||
- :ref:`Simple Universe <simple_universe>`: This code provides the 'universe'
|
|
||||||
of available trading pairs on a given exchange on any given day. You can use
|
|
||||||
this code to dynamically select which currency pairs you want to trade each
|
|
||||||
day of your strategy. This example does not make any trades.
|
|
||||||
|
|
||||||
- :ref:`Portfolio Optimization <portfolio_optimization>`: Use this code to
|
- :ref:`Portfolio Optimization <portfolio_optimization>`: Use this code to
|
||||||
execute a portfolio optimization model. This strategy will select the
|
execute a portfolio optimization model. This strategy will select the
|
||||||
portfolio with the maximum Sharpe Ratio. The parameters are set to use 180
|
portfolio with the maximum Sharpe Ratio. The parameters are set to use 180
|
||||||
@@ -184,6 +179,7 @@ one day prior to the current date.
|
|||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
record(
|
record(
|
||||||
@@ -757,177 +753,6 @@ implemented after the video was recorded, which executes the orders at slighlty
|
|||||||
different prices, but resulting in significant changes in performance of our
|
different prices, but resulting in significant changes in performance of our
|
||||||
strategy.
|
strategy.
|
||||||
|
|
||||||
.. _simple_universe:
|
|
||||||
|
|
||||||
Simple Universe
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Source code: `examples/simple_universe.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/simple_universe.py>`_
|
|
||||||
|
|
||||||
This example aims to provide an easy way for users to learn how to
|
|
||||||
collect data from any given exchange and select a subset of the available
|
|
||||||
currency pairs for trading. You simply need to specify the exchange and
|
|
||||||
the market (base_currency) that you want to focus on. You will then see
|
|
||||||
how to create a universe of assets, and filter it based the market you
|
|
||||||
desire.
|
|
||||||
|
|
||||||
The example prints out the closing price of all the pairs for a given
|
|
||||||
market in a given exchange every 30 minutes. The example also contains
|
|
||||||
the OHLCV data with minute-resolution for the past seven days which
|
|
||||||
could be used to create indicators. Use this code as the backbone to
|
|
||||||
create your own trading strategy.
|
|
||||||
|
|
||||||
The lookback_date variable is used to ensure data for a coin existed on
|
|
||||||
the lookback period specified.
|
|
||||||
|
|
||||||
To run, execute the following two commands in a terminal (inside catalyst
|
|
||||||
environment). The first one retrieves all the pricing data needed for this
|
|
||||||
script to run (only needs to be run once), and the second one executes this
|
|
||||||
script with the parameters specified in the run_algorithm() call at the end
|
|
||||||
of the file:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
catalyst ingest-exchange -x bitfinex -f minute
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
python simple_universe.py
|
|
||||||
|
|
||||||
Credits: This code was originally submitted by `Abner Ayala-Acevedo
|
|
||||||
<https://github.com/abnera>`_. Thank you!
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
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 # minute counter
|
|
||||||
context.exchange = context.exchanges.values()[0].name.lower()
|
|
||||||
context.base_currency = context.exchanges.values()[0].base_currency.lower()
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
context.i += 1
|
|
||||||
lookback_days = 7 # 7 days
|
|
||||||
|
|
||||||
# current date & time in each iteration formatted into a string
|
|
||||||
now = data.current_dt
|
|
||||||
date, time = now.strftime('%Y-%m-%d %H:%M:%S').split(' ')
|
|
||||||
lookback_date = now - timedelta(days=lookback_days)
|
|
||||||
# keep only the date as a string, discard the time
|
|
||||||
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[0]
|
|
||||||
|
|
||||||
one_day_in_minutes = 1440 # 60 * 24 assumes data_frequency='minute'
|
|
||||||
# update universe everyday at midnight
|
|
||||||
if not context.i % one_day_in_minutes:
|
|
||||||
context.universe = universe(context, lookback_date, date)
|
|
||||||
|
|
||||||
# get data every 30 minutes
|
|
||||||
minutes = 30
|
|
||||||
# get lookback_days of history data: that is 'lookback' number of bins
|
|
||||||
lookback = one_day_in_minutes / minutes * lookback_days
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Get 30 minute interval OHLCV data. This is the standard data
|
|
||||||
# required for candlestick or indicators/signals. Return Pandas
|
|
||||||
# DataFrames. 30T means 30-minute re-sampling of one minute data.
|
|
||||||
# Adjust it to your desired time interval as needed.
|
|
||||||
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 last value in the set, which is the equivalent
|
|
||||||
# to current price (as in the most recent value)
|
|
||||||
# displays the minute price for each pair every 30 minutes
|
|
||||||
print('{now}: {pair} -\tO:{o},\tH:{h},\tL:{c},\tC{c},\tV:{v}'.format(
|
|
||||||
now=now,
|
|
||||||
pair=pair,
|
|
||||||
o=opened[-1],
|
|
||||||
h=high[-1],
|
|
||||||
l=low[-1],
|
|
||||||
c=close[-1],
|
|
||||||
v=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):
|
|
||||||
# get all the pairs for the given exchange
|
|
||||||
json_symbols = get_exchange_symbols(context.exchange)
|
|
||||||
# convert into a DataFrame for easier processing
|
|
||||||
df = pd.DataFrame.from_dict(json_symbols).transpose().astype(str)
|
|
||||||
df['base_currency'] = df.apply(lambda row: row.symbol.split('_')[1],axis=1)
|
|
||||||
df['market_currency'] = df.apply(lambda row: row.symbol.split('_')[0],axis=1)
|
|
||||||
|
|
||||||
# Filter all the pairs to get only the ones for a given base_currency
|
|
||||||
df = df[df['base_currency'] == context.base_currency]
|
|
||||||
|
|
||||||
# Filter all the pairs to ensure that pair existed in the current date range
|
|
||||||
df = df[df.start_date < lookback_date]
|
|
||||||
df = df[df.end_daily >= current_date]
|
|
||||||
context.coins = symbols(*df.symbol) # convert all the pairs to symbols
|
|
||||||
|
|
||||||
return 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
|
|
||||||
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')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. _portfolio_optimization:
|
.. _portfolio_optimization:
|
||||||
|
|
||||||
Portfolio Optimization
|
Portfolio Optimization
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.. include:: ../../README.rst
|
.. include:: welcome.rst
|
||||||
|
|
|
|
||||||
|
|
|
|
||||||
Table of Contents
|
Table of Contents
|
||||||
@@ -9,8 +9,9 @@ Table of Contents
|
|||||||
|
|
||||||
install
|
install
|
||||||
beginner-tutorial
|
beginner-tutorial
|
||||||
|
jupyter
|
||||||
live-trading
|
live-trading
|
||||||
features
|
naming-convention
|
||||||
example-algos
|
example-algos
|
||||||
utilities
|
utilities
|
||||||
videos
|
videos
|
||||||
@@ -18,6 +19,7 @@ Table of Contents
|
|||||||
development-guidelines
|
development-guidelines
|
||||||
releases
|
releases
|
||||||
.. bundles
|
.. bundles
|
||||||
|
.. development-guidelines
|
||||||
.. appendix
|
.. appendix
|
||||||
.. release-process
|
.. release-process
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ as an alternative installation method for MacOS and Linux, you can install
|
|||||||
Catalyst directly with ``pip`` (we recommend in combination with a virtual
|
Catalyst directly with ``pip`` (we recommend in combination with a virtual
|
||||||
environemnt). See :ref:`Installing with pip <pip>`.
|
environemnt). See :ref:`Installing with pip <pip>`.
|
||||||
|
|
||||||
Alternatively you can install Catalyst using ``pipenv`` which is a mix of pip
|
|
||||||
and virtualenv. See :ref:`Installing with pipenv <pipenv>`.
|
|
||||||
|
|
||||||
Regardless of the method, each operating system (OS), has its own
|
Regardless of the method, each operating system (OS), has its own
|
||||||
prerequisites, make sure to review the corresponding sections for your system:
|
prerequisites, make sure to review the corresponding sections for your system:
|
||||||
:ref:`Linux <linux>`, :ref:`MacOS <macos>` and :ref:`Windows <windows>`.
|
:ref:`Linux <linux>`, :ref:`MacOS <macos>` and :ref:`Windows <windows>`.
|
||||||
@@ -296,39 +293,6 @@ Troubleshooting ``pip`` Install
|
|||||||
|
|
||||||
sudo apt-get install python-dev
|
sudo apt-get install python-dev
|
||||||
|
|
||||||
.. _pipenv:
|
|
||||||
|
|
||||||
Installing with ``pipenv``
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
Installing Catalyst via ``pipenv`` is perhaps easier that installing it via
|
|
||||||
``pip`` itself but you need to install ``pipenv`` first via ``pip``.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ pip install pipenv
|
|
||||||
|
|
||||||
Once ``pipenv`` is installed you can proceed by creating a project folder and
|
|
||||||
installing Catalyst on that project automagically as follows:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ mkdir project
|
|
||||||
$ cd project
|
|
||||||
$ pipenv --two
|
|
||||||
$ pipenv install enigma-catalyst matplotlib
|
|
||||||
|
|
||||||
Until now the workflow compared to ``pip`` is almost identical, the difference
|
|
||||||
is that you don't need to load manually any virtualenv however you need to use
|
|
||||||
the `pipenv run` prefix to run the `catalyst` command as follows:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ pipenv run catalyst --version
|
|
||||||
|
|
||||||
If you want to know more about ``pipenv`` go to the `pipenv github repo`_
|
|
||||||
|
|
||||||
.. _`pipenv github repo`: https://github.com/pypa/pipenv
|
|
||||||
|
|
||||||
.. _linux:
|
.. _linux:
|
||||||
|
|
||||||
|
|||||||
+15794
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user