mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-22 12:40:30 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d3c53dbef | ||
|
|
04a1513e73 | ||
|
|
03d2c0e306 | ||
|
|
853707dfb2 | ||
|
|
540dd97dbf | ||
|
|
9f372828b4 | ||
|
|
6a929b6e25 | ||
|
|
45f278ab15 | ||
|
|
a58fa21234 | ||
|
|
c8d6e07179 | ||
|
|
fdfc3f2ec3 | ||
|
|
3a321eb195 | ||
|
|
d21eb3b946 | ||
|
|
439b5404ae | ||
|
|
821f60897f | ||
|
|
5ff935723f | ||
|
|
51126fd7ae | ||
|
|
fec6a159e6 | ||
|
|
eee9a07f54 | ||
|
|
563fc433d5 | ||
|
|
53a54fde7c | ||
|
|
b0f2202b54 | ||
|
|
cf6c3bb76b | ||
|
|
772640e098 | ||
|
|
52d4ced37c | ||
|
|
3ed44f72ad | ||
|
|
7569f7eb7c | ||
|
|
6d28e289c4 | ||
|
|
22154f2337 | ||
|
|
5314d3e1f8 | ||
|
|
bb16975400 | ||
|
|
0ce624e6f6 | ||
|
|
50dc322230 | ||
|
|
fb36435231 | ||
|
|
e688783931 | ||
|
|
2f3dbeedcd | ||
|
|
cebb1cd6d4 | ||
|
|
020ec50258 | ||
|
|
df42cc9047 | ||
|
|
8a9b4e2df7 | ||
|
|
b9150aab79 | ||
|
|
d4efed0d3a | ||
|
|
db1ad9aac8 | ||
|
|
270c261203 | ||
|
|
3f974b1adf | ||
|
|
9eac88344d | ||
|
|
5d1f00bd19 | ||
|
|
8d3f3ba81d | ||
|
|
248299a725 | ||
|
|
e916283522 | ||
|
|
b7a32656d5 | ||
|
|
78eb5d9d64 | ||
|
|
14c7170159 | ||
|
|
415fceb11c | ||
|
|
05c8957c90 | ||
|
|
e4bacb169e | ||
|
|
608dd843d6 | ||
|
|
e0827fe4ad | ||
|
|
050fda1bdb | ||
|
|
713d487808 | ||
|
|
ff7d7c5256 | ||
|
|
b60b50e99a | ||
|
|
93ebbf8b1f | ||
|
|
db37c9c6a7 | ||
|
|
a49cb55821 | ||
|
|
ac15413af8 | ||
|
|
141ee65c91 | ||
|
|
55d1fee82d | ||
|
|
33f94b3ef9 | ||
|
|
30448a65c5 | ||
|
|
9c33ee123c | ||
|
|
d88710501f | ||
|
|
ba0208910f | ||
|
|
5d9708901d | ||
|
|
96b36c6614 | ||
|
|
13de3e69ef | ||
|
|
215de33c35 | ||
|
|
790ac22f8d | ||
|
|
4d8d1e33d0 | ||
|
|
595bd82234 | ||
|
|
9515d10cef | ||
|
|
56481bbbe0 | ||
|
|
79e4854973 | ||
|
|
b4e7629e8b | ||
|
|
a818d10d40 | ||
|
|
bd4b0d2756 | ||
|
|
80e29b2aa4 | ||
|
|
54ebfd6aad | ||
|
|
606e2d5278 | ||
|
|
194b96f5c1 | ||
|
|
91dcd8d83b | ||
|
|
0396dee74d | ||
|
|
3045c5108a | ||
|
|
9bb12eb781 | ||
|
|
d64fe12751 | ||
|
|
f5503ae717 | ||
|
|
4c2a727cd7 | ||
|
|
0605fca115 | ||
|
|
a60311b002 | ||
|
|
bd2ee45664 | ||
|
|
89f7060a80 | ||
|
|
15d0337b34 | ||
|
|
8d4caa2a1a | ||
|
|
5aef794f97 | ||
|
|
d01d8d5bbc | ||
|
|
aaced362a0 | ||
|
|
178801d3a7 | ||
|
|
6931dae8fd | ||
|
|
036aab07e1 | ||
|
|
e61017a86f | ||
|
|
39065f9dfa | ||
|
|
6069673cdd | ||
|
|
e3ad6558a5 | ||
|
|
74c10c45a0 | ||
|
|
35fb862fa9 | ||
|
|
8f9b1af729 | ||
|
|
1e3a0dddf6 | ||
|
|
e8c4a3636a | ||
|
|
81eaa6426a | ||
|
|
0a946e4200 | ||
|
|
0a9137fbf9 | ||
|
|
8a1d914ed9 | ||
|
|
cf89e01a51 | ||
|
|
3b10a59572 | ||
|
|
4a904a0106 | ||
|
|
24d7f52d46 | ||
|
|
8ab3382f47 | ||
|
|
2bd54202ce | ||
|
|
2e8dd3840b | ||
|
|
b1f49d3c8d | ||
|
|
8e232ac5ef | ||
|
|
ac2f0bbbf8 | ||
|
|
cd1f5ca97b | ||
|
|
44614a75c2 | ||
|
|
8c028e75a0 | ||
|
|
3a7128ad4f | ||
|
|
869ef8ec87 | ||
|
|
683f24b24f | ||
|
|
332a3d41e3 | ||
|
|
e9714cfb32 | ||
|
|
0408899998 | ||
|
|
67bd5c8f6a | ||
|
|
662af595cb | ||
|
|
555b1d817e | ||
|
|
d289a1cba5 | ||
|
|
3b16bf7538 | ||
|
|
55262eeb46 | ||
|
|
5d8d14640c | ||
|
|
99db2ca46d | ||
|
|
9b382f58b8 | ||
|
|
b1a735d83f | ||
|
|
15e963cee1 | ||
|
|
1921c3dc80 | ||
|
|
757b8a0eef | ||
|
|
2e4c9fe027 | ||
|
|
35e71559a4 | ||
|
|
6bfd76ffc7 | ||
|
|
e536b88f1c | ||
|
|
ff1df11384 | ||
|
|
dcefca6038 | ||
|
|
6aefc71449 | ||
|
|
394afdba6d | ||
|
|
c698956f9f | ||
|
|
1ff9c9f39b | ||
|
|
b93f9c10d3 | ||
|
|
5962496d83 | ||
|
|
952ccf37fa | ||
|
|
4740aebd7f | ||
|
|
2a9fe7dbe2 | ||
|
|
44753b681d | ||
|
|
865ca54e86 | ||
|
|
71654ff9ff | ||
|
|
95ce66a66a | ||
|
|
4ed000609d | ||
|
|
024342ce12 | ||
|
|
24460967b3 | ||
|
|
94583c2684 | ||
|
|
7f7cb80e37 | ||
|
|
263a2ba547 | ||
|
|
3014651ac2 | ||
|
|
a7c9846245 | ||
|
|
d41d9095a1 | ||
|
|
ddf0c480a0 | ||
|
|
7091546b2b | ||
|
|
a7bcf063c3 | ||
|
|
f2e4637f29 | ||
|
|
021e1fd8c8 | ||
|
|
e46604707f | ||
|
|
eee8dcbbd6 | ||
|
|
025929035e | ||
|
|
1d15e12b8d | ||
|
|
552f4260b4 | ||
|
|
0c3b5fc3c5 | ||
|
|
42f59e99df | ||
|
|
5d84f26b72 | ||
|
|
60747082b6 | ||
|
|
1e00be4cf3 | ||
|
|
153469664c | ||
|
|
d40e9f1623 | ||
|
|
4f8bf413bd | ||
|
|
0558cc61cf | ||
|
|
a3761beae2 | ||
|
|
49aaaa8f26 | ||
|
|
a74da31964 | ||
|
|
e41eca0d8a | ||
|
|
48f4d01c70 | ||
|
|
6981669a68 | ||
|
|
ebd1ca44f6 | ||
|
|
12f0f4319b | ||
|
|
4c15f5efda | ||
|
|
ce61f49f27 | ||
|
|
313db1def9 | ||
|
|
57f6a69e94 | ||
|
|
ce085e01ec | ||
|
|
89f9a1179e | ||
|
|
eb5d55478d | ||
|
|
0855391f77 | ||
|
|
619fbfc6ea | ||
|
|
b644c947e3 | ||
|
|
1c03d837cc | ||
|
|
2e7aabd973 | ||
|
|
6147df5c6c | ||
|
|
54dcc58ee8 | ||
|
|
ff94c735e8 | ||
|
|
aa3f75390d | ||
|
|
f4a1ad6e61 | ||
|
|
87428b299f | ||
|
|
5b78f161a4 | ||
|
|
9688d71e23 | ||
|
|
02875ef7ab | ||
|
|
e42276affa | ||
|
|
f47aa13dd3 | ||
|
|
dcdf4f77db | ||
|
|
96a27d083c | ||
|
|
f995f451a7 | ||
|
|
a3838fc00f | ||
|
|
4db8131397 | ||
|
|
207bce6216 | ||
|
|
8fb8b80a12 | ||
|
|
b762689225 | ||
|
|
7bbb6e0b42 | ||
|
|
b587804e3e | ||
|
|
5660247da2 | ||
|
|
fc2c44a6b7 | ||
|
|
4eb8a6eb0f | ||
|
|
c8eaa11f80 | ||
|
|
55a9d76b9b | ||
|
|
7abd992d17 | ||
|
|
daeccaed36 | ||
|
|
25358a4077 | ||
|
|
7cf4f84e89 | ||
|
|
b63199e4e1 | ||
|
|
4af08be7e8 | ||
|
|
606148e19c | ||
|
|
d38e560265 | ||
|
|
ffd1bc07cc | ||
|
|
dfcfe5a370 | ||
|
|
803823eac0 | ||
|
|
5a18e09730 | ||
|
|
dd41f8c006 | ||
|
|
7a2a4817fe | ||
|
|
105522e5ab | ||
|
|
a52e201f86 | ||
|
|
697ff54125 | ||
|
|
4bcd34bd78 | ||
|
|
64c52c7a3c | ||
|
|
1c143eb9ea | ||
|
|
1da4ccfb8c | ||
|
|
a61b22b821 | ||
|
|
d07e0edd88 | ||
|
|
c2821ab77b | ||
|
|
1696db930d | ||
|
|
9397b3fd5a | ||
|
|
2bb11db412 | ||
|
|
c2f3e00d99 | ||
|
|
292fe66d3f | ||
|
|
ba46015bae | ||
|
|
12d5915c8e | ||
|
|
c6fe45371c | ||
|
|
2fdb4dd0bd | ||
|
|
968e70b69b | ||
|
|
6a7c47f3a9 | ||
|
|
7daf295e63 | ||
|
|
7dddc0a85f | ||
|
|
32523d474d | ||
|
|
841acf0203 | ||
|
|
b4ab1a5375 | ||
|
|
3ec9853b75 | ||
|
|
c1d140a831 | ||
|
|
02dc4d6a30 | ||
|
|
0d366a350d | ||
|
|
1e8b0c36a1 | ||
|
|
0af592a5f4 | ||
|
|
86b2a5c772 | ||
|
|
9cfd50dc4f | ||
|
|
698b19c8fa | ||
|
|
5d4bc99097 | ||
|
|
cfb3f1ca42 | ||
|
|
01b02ccd84 | ||
|
|
ee1605a5e6 | ||
|
|
f3dca74e87 | ||
|
|
d57b79427b | ||
|
|
8a89c0c53f | ||
|
|
a8a869dd49 | ||
|
|
c260e188b0 | ||
|
|
230b9c17eb | ||
|
|
2a8b5cf911 | ||
|
|
3fa88a3e56 | ||
|
|
64532c3d08 | ||
|
|
5f86ab659e | ||
|
|
df14a94918 | ||
|
|
e087e48088 | ||
|
|
5110b37a82 | ||
|
|
a2bb231424 | ||
|
|
e939f742a8 | ||
|
|
9093be748e | ||
|
|
e23a7e67a0 | ||
|
|
273b4fb7a7 | ||
|
|
0b2684d532 | ||
|
|
224192a1ee | ||
|
|
2d7202ac81 | ||
|
|
8d95428fa6 | ||
|
|
d678376d8d | ||
|
|
9b5fa83da3 | ||
|
|
0f1c3e1ace | ||
|
|
f3cb610748 | ||
|
|
1e6316d414 | ||
|
|
df51cbe21b | ||
|
|
dce31b212b | ||
|
|
00269d3dfb | ||
|
|
648be3969a | ||
|
|
a54325fdcf | ||
|
|
b64e5929b4 | ||
|
|
631cbcd352 | ||
|
|
24c5a5bd13 | ||
|
|
1103947af0 | ||
|
|
061de3c12f | ||
|
|
207887a28d | ||
|
|
dc53f973e4 | ||
|
|
9a80a488cd | ||
|
|
a85b6c798a | ||
|
|
9229809b05 | ||
|
|
f81cf6b600 | ||
|
|
ba4ffc7272 | ||
|
|
12695474e3 | ||
|
|
cd0157347f | ||
|
|
76a8362e3d | ||
|
|
c8cc2edd36 | ||
|
|
cb870422c3 |
@@ -40,6 +40,7 @@ develop-eggs
|
|||||||
coverage.xml
|
coverage.xml
|
||||||
htmlcov
|
htmlcov
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
|
.python-version
|
||||||
|
|
||||||
# C Extensions
|
# C Extensions
|
||||||
*.o
|
*.o
|
||||||
|
|||||||
+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
|
||||||
#
|
#
|
||||||
|
|||||||
+72
-3
@@ -1,3 +1,72 @@
|
|||||||
All the documentation for `Catalyst <https://github.com/enigmampc/catalyst>`_
|
.. image:: https://s3.amazonaws.com/enigmaco-docs/enigma-catalyst.jpg
|
||||||
can be found in the
|
:target: https://enigmampc.github.io/catalyst
|
||||||
`documentation website <https://enigmampc.github.io/catalyst>`_.
|
:align: center
|
||||||
|
: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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-10
@@ -29,11 +29,14 @@ 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(
|
||||||
@@ -44,10 +47,6 @@ 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')
|
||||||
@@ -69,7 +68,6 @@ if os.name == 'nt':
|
|||||||
_()
|
_()
|
||||||
del _
|
del _
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'TradingAlgorithm',
|
'TradingAlgorithm',
|
||||||
'api',
|
'api',
|
||||||
@@ -80,7 +78,3 @@ __all__ = [
|
|||||||
'run_algorithm',
|
'run_algorithm',
|
||||||
'utils',
|
'utils',
|
||||||
]
|
]
|
||||||
|
|
||||||
from ._version import get_versions
|
|
||||||
__version__ = get_versions()['version']
|
|
||||||
del get_versions
|
|
||||||
|
|||||||
+120
-43
@@ -9,7 +9,7 @@ from six import text_type
|
|||||||
|
|
||||||
from catalyst.data import bundles as bundles_module
|
from catalyst.data import bundles as bundles_module
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
from catalyst.exchange.factory import get_exchange
|
from catalyst.exchange.utils.exchange_utils import delete_algo_folder
|
||||||
from catalyst.utils.cli import Date, Timestamp
|
from catalyst.utils.cli import Date, Timestamp
|
||||||
from catalyst.utils.run_algo import _run, load_extensions
|
from catalyst.utils.run_algo import _run, load_extensions
|
||||||
|
|
||||||
@@ -29,16 +29,17 @@ except NameError:
|
|||||||
@click.option(
|
@click.option(
|
||||||
'--strict-extensions/--non-strict-extensions',
|
'--strict-extensions/--non-strict-extensions',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help='If --strict-extensions is passed then catalyst will not run if it'
|
help='If --strict-extensions is passed then catalyst will not run '
|
||||||
' cannot load all of the specified extensions. If this is not passed or'
|
'if it cannot load all of the specified extensions. If this is '
|
||||||
' --non-strict-extensions is passed then the failure will be logged but'
|
'not passed or --non-strict-extensions is passed then the '
|
||||||
' execution will continue.',
|
'failure will be logged but execution will continue.',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--default-extension/--no-default-extension',
|
'--default-extension/--no-default-extension',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=True,
|
default=True,
|
||||||
help="Don't load the default catalyst extension.py file in $CATALYST_HOME.",
|
help="Don't load the default catalyst extension.py file "
|
||||||
|
"in $CATALYST_HOME.",
|
||||||
)
|
)
|
||||||
@click.version_option()
|
@click.version_option()
|
||||||
def main(extension, strict_extensions, default_extension):
|
def main(extension, strict_extensions, default_extension):
|
||||||
@@ -123,9 +124,9 @@ def ipython_only(option):
|
|||||||
'--define',
|
'--define',
|
||||||
multiple=True,
|
multiple=True,
|
||||||
help="Define a name to be bound in the namespace before executing"
|
help="Define a name to be bound in the namespace before executing"
|
||||||
" the algotext. For example '-Dname=value'. The value may be any python"
|
" the algotext. For example '-Dname=value'. The value may be"
|
||||||
" expression. These are evaluated in order so they may refer to previously"
|
" any python expression. These are evaluated in order so they"
|
||||||
" defined names.",
|
" may refer to previously defined names.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--data-frequency',
|
'--data-frequency',
|
||||||
@@ -137,7 +138,6 @@ def ipython_only(option):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'--capital-base',
|
'--capital-base',
|
||||||
type=float,
|
type=float,
|
||||||
default=10e6,
|
|
||||||
show_default=True,
|
show_default=True,
|
||||||
help='The starting capital for the simulation.',
|
help='The starting capital for the simulation.',
|
||||||
)
|
)
|
||||||
@@ -175,8 +175,8 @@ def ipython_only(option):
|
|||||||
default='-',
|
default='-',
|
||||||
metavar='FILENAME',
|
metavar='FILENAME',
|
||||||
show_default=True,
|
show_default=True,
|
||||||
help="The location to write the perf data. If this is '-' the perf will"
|
help="The location to write the perf data. If this is '-' the perf"
|
||||||
" be written to stdout.",
|
" will be written to stdout.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--print-algo/--no-print-algo',
|
'--print-algo/--no-print-algo',
|
||||||
@@ -193,8 +193,7 @@ def ipython_only(option):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
help='The name of the targeted exchange.',
|
||||||
help='The name of the targeted exchange (supported: bitfinex, bittrex, poloniex).',
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-n',
|
'-n',
|
||||||
@@ -239,16 +238,27 @@ def run(ctx,
|
|||||||
# does not pass either of these and then passes the first only
|
# does not pass either of these and then passes the first only
|
||||||
# to be told they need to pass the second argument also
|
# to be told they need to pass the second argument also
|
||||||
ctx.fail(
|
ctx.fail(
|
||||||
"must specify dates with '-s' / '--start' and '-e' / '--end'",
|
"must specify dates with '-s' / '--start' and '-e' / '--end'"
|
||||||
|
" in backtest mode",
|
||||||
)
|
)
|
||||||
if start is None:
|
if start is None:
|
||||||
ctx.fail("must specify a start date with '-s' / '--start'")
|
ctx.fail("must specify a start date with '-s' / '--start'"
|
||||||
|
" in backtest mode")
|
||||||
if end is None:
|
if end is None:
|
||||||
ctx.fail("must specify an end date with '-e' / '--end'")
|
ctx.fail("must specify an end date with '-e' / '--end'"
|
||||||
|
" in backtest mode")
|
||||||
|
|
||||||
if exchange_name is None:
|
if exchange_name is None:
|
||||||
ctx.fail("must specify an exchange name '-x'")
|
ctx.fail("must specify an exchange name '-x'")
|
||||||
|
|
||||||
|
if base_currency is None:
|
||||||
|
ctx.fail("must specify a base currency with '-c' in backtest mode")
|
||||||
|
|
||||||
|
if capital_base is None:
|
||||||
|
ctx.fail("must specify a capital base with '--capital-base'")
|
||||||
|
|
||||||
|
click.echo('Running in backtesting mode.')
|
||||||
|
|
||||||
perf = _run(
|
perf = _run(
|
||||||
initialize=None,
|
initialize=None,
|
||||||
handle_data=None,
|
handle_data=None,
|
||||||
@@ -272,7 +282,10 @@ 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
|
analyze_live=None,
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=True,
|
||||||
|
stats_output=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if output == '-':
|
if output == '-':
|
||||||
@@ -324,6 +337,12 @@ 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',
|
||||||
@@ -334,9 +353,9 @@ def catalyst_magic(line, cell=None):
|
|||||||
'--define',
|
'--define',
|
||||||
multiple=True,
|
multiple=True,
|
||||||
help="Define a name to be bound in the namespace before executing"
|
help="Define a name to be bound in the namespace before executing"
|
||||||
" the algotext. For example '-Dname=value'. The value may be any python"
|
" the algotext. For example '-Dname=value'. The value may be"
|
||||||
" expression. These are evaluated in order so they may refer to previously"
|
" any python expression. These are evaluated in order so they"
|
||||||
" defined names.",
|
" may refer to previously defined names.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-o',
|
'-o',
|
||||||
@@ -362,8 +381,7 @@ def catalyst_magic(line, cell=None):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
help='The name of the targeted exchange.',
|
||||||
help='The name of the targeted exchange (supported: bitfinex, bittrex, poloniex).',
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-n',
|
'-n',
|
||||||
@@ -376,15 +394,29 @@ def catalyst_magic(line, cell=None):
|
|||||||
help='The base currency used to calculate statistics '
|
help='The base currency used to calculate statistics '
|
||||||
'(e.g. usd, btc, eth).',
|
'(e.g. usd, btc, eth).',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'-e',
|
||||||
|
'--end',
|
||||||
|
type=Date(tz='utc', as_timestamp=True),
|
||||||
|
help='An optional end date at which to stop the execution.',
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--live-graph/--no-live-graph',
|
'--live-graph/--no-live-graph',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
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,
|
||||||
@@ -393,7 +425,9 @@ def live(ctx,
|
|||||||
exchange_name,
|
exchange_name,
|
||||||
algo_namespace,
|
algo_namespace,
|
||||||
base_currency,
|
base_currency,
|
||||||
live_graph):
|
end,
|
||||||
|
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):
|
||||||
@@ -404,11 +438,22 @@ 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,
|
||||||
@@ -418,12 +463,12 @@ def live(ctx,
|
|||||||
algotext=algotext,
|
algotext=algotext,
|
||||||
defines=define,
|
defines=define,
|
||||||
data_frequency=None,
|
data_frequency=None,
|
||||||
capital_base=None,
|
capital_base=capital_base,
|
||||||
data=None,
|
data=None,
|
||||||
bundle=None,
|
bundle=None,
|
||||||
bundle_timestamp=None,
|
bundle_timestamp=None,
|
||||||
start=None,
|
start=None,
|
||||||
end=None,
|
end=end,
|
||||||
output=output,
|
output=output,
|
||||||
print_algo=print_algo,
|
print_algo=print_algo,
|
||||||
local_namespace=local_namespace,
|
local_namespace=local_namespace,
|
||||||
@@ -432,7 +477,10 @@ 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,
|
||||||
|
analyze_live=None,
|
||||||
|
simulate_orders=simulate_orders,
|
||||||
|
stats_output=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if output == '-':
|
if output == '-':
|
||||||
@@ -447,9 +495,7 @@ def live(ctx,
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
help='The name of the exchange bundle to ingest.',
|
||||||
help='The name of the exchange bundle to ingest (supported: bitfinex,'
|
|
||||||
' bittrex, poloniex).',
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-f',
|
'-f',
|
||||||
@@ -485,13 +531,32 @@ def live(ctx,
|
|||||||
help='A list of symbols to exclude from the ingestion '
|
help='A list of symbols to exclude from the ingestion '
|
||||||
'(optional comma separated list)',
|
'(optional comma separated list)',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'--csv',
|
||||||
|
default=None,
|
||||||
|
help='The path of a CSV file containing the data. If specified, start, '
|
||||||
|
'end, include-symbols and exclude-symbols will be ignored. Instead,'
|
||||||
|
'all data in the file will be ingested.',
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--show-progress/--no-show-progress',
|
'--show-progress/--no-show-progress',
|
||||||
default=True,
|
default=True,
|
||||||
help='Print progress information to the terminal.'
|
help='Print progress information to the terminal.'
|
||||||
)
|
)
|
||||||
def ingest_exchange(exchange_name, data_frequency, start, end,
|
@click.option(
|
||||||
include_symbols, exclude_symbols, show_progress):
|
'--verbose/--no-verbose`',
|
||||||
|
default=False,
|
||||||
|
help='Show a progress indicator for every currency pair.'
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--validate/--no-validate`',
|
||||||
|
default=False,
|
||||||
|
help='Report potential anomalies found in data bundles.'
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def ingest_exchange(ctx, exchange_name, data_frequency, start, end,
|
||||||
|
include_symbols, exclude_symbols, csv, show_progress,
|
||||||
|
verbose, validate):
|
||||||
"""
|
"""
|
||||||
Ingest data for the given exchange.
|
Ingest data for the given exchange.
|
||||||
"""
|
"""
|
||||||
@@ -499,8 +564,7 @@ def ingest_exchange(exchange_name, data_frequency, start, end,
|
|||||||
if exchange_name is None:
|
if exchange_name is None:
|
||||||
ctx.fail("must specify an exchange name '-x'")
|
ctx.fail("must specify an exchange name '-x'")
|
||||||
|
|
||||||
exchange = get_exchange(exchange_name)
|
exchange_bundle = ExchangeBundle(exchange_name)
|
||||||
exchange_bundle = ExchangeBundle(exchange)
|
|
||||||
|
|
||||||
click.echo('Ingesting exchange bundle {}...'.format(exchange_name))
|
click.echo('Ingesting exchange bundle {}...'.format(exchange_name))
|
||||||
exchange_bundle.ingest(
|
exchange_bundle.ingest(
|
||||||
@@ -509,17 +573,33 @@ def ingest_exchange(exchange_name, data_frequency, start, end,
|
|||||||
exclude_symbols=exclude_symbols,
|
exclude_symbols=exclude_symbols,
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
show_progress=show_progress
|
show_progress=show_progress,
|
||||||
|
show_breakdown=verbose,
|
||||||
|
show_report=validate,
|
||||||
|
csv=csv
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command(name='clean-algo')
|
||||||
|
@click.option(
|
||||||
|
'-n',
|
||||||
|
'--algo-namespace',
|
||||||
|
help='The label of the algorithm to for which to clean the state.'
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def clean_algo(ctx, algo_namespace):
|
||||||
|
click.echo(
|
||||||
|
'Cleaning algo state: {}'.format(algo_namespace)
|
||||||
|
)
|
||||||
|
delete_algo_folder(algo_namespace)
|
||||||
|
click.echo('Done')
|
||||||
|
|
||||||
|
|
||||||
@main.command(name='clean-exchange')
|
@main.command(name='clean-exchange')
|
||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
help='The name of the exchange bundle to ingest.',
|
||||||
help='The name of the exchange bundle to ingest (supported: bitfinex,'
|
|
||||||
' bittrex, poloniex).',
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-f',
|
'-f',
|
||||||
@@ -537,8 +617,7 @@ def clean_exchange(ctx, exchange_name, data_frequency):
|
|||||||
if exchange_name is None:
|
if exchange_name is None:
|
||||||
ctx.fail("must specify an exchange name '-x'")
|
ctx.fail("must specify an exchange name '-x'")
|
||||||
|
|
||||||
exchange = get_exchange(exchange_name)
|
exchange_bundle = ExchangeBundle(exchange_name)
|
||||||
exchange_bundle = ExchangeBundle(exchange)
|
|
||||||
|
|
||||||
click.echo('Cleaning exchange bundle {}...'.format(exchange_name))
|
click.echo('Cleaning exchange bundle {}...'.format(exchange_name))
|
||||||
exchange_bundle.clean(
|
exchange_bundle.clean(
|
||||||
@@ -559,9 +638,7 @@ def clean_exchange(ctx, exchange_name, data_frequency):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
help='The name of the exchange bundle to ingest.',
|
||||||
help='The name of the exchange bundle to ingest (supported: bitfinex,'
|
|
||||||
' bittrex, poloniex).',
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-c',
|
'-c',
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ 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
|
||||||
@@ -1485,7 +1484,6 @@ 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)
|
||||||
|
|
||||||
@@ -1523,6 +1521,7 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
+75
-19
@@ -17,6 +17,7 @@
|
|||||||
"""
|
"""
|
||||||
Cythonized Asset object.
|
Cythonized Asset object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
cimport cython
|
cimport cython
|
||||||
@@ -38,6 +39,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.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
|
||||||
|
|
||||||
@@ -395,11 +397,18 @@ cdef class Future(Asset):
|
|||||||
|
|
||||||
cdef class TradingPair(Asset):
|
cdef class TradingPair(Asset):
|
||||||
cdef readonly float leverage
|
cdef readonly float leverage
|
||||||
cdef readonly object market_currency
|
cdef readonly object quote_currency
|
||||||
cdef readonly object base_currency
|
cdef readonly object base_currency
|
||||||
cdef readonly object end_daily
|
cdef readonly object end_daily
|
||||||
cdef readonly object end_minute
|
cdef readonly object end_minute
|
||||||
cdef readonly object exchange_symbol
|
cdef readonly object exchange_symbol
|
||||||
|
cdef readonly float maker
|
||||||
|
cdef readonly float taker
|
||||||
|
cdef readonly int trading_state
|
||||||
|
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',
|
||||||
@@ -412,12 +421,19 @@ cdef class TradingPair(Asset):
|
|||||||
'exchange',
|
'exchange',
|
||||||
'exchange_full',
|
'exchange_full',
|
||||||
'leverage',
|
'leverage',
|
||||||
'market_currency',
|
'quote_currency',
|
||||||
'base_currency',
|
'base_currency',
|
||||||
'end_daily',
|
'end_daily',
|
||||||
'end_minute',
|
'end_minute',
|
||||||
'exchange_symbol',
|
'exchange_symbol',
|
||||||
'min_trade_size'
|
'min_trade_size',
|
||||||
|
'max_trade_size',
|
||||||
|
'lot',
|
||||||
|
'maker',
|
||||||
|
'taker',
|
||||||
|
'trading_state',
|
||||||
|
'data_source',
|
||||||
|
'decimals'
|
||||||
})
|
})
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
object symbol,
|
object symbol,
|
||||||
@@ -433,10 +449,17 @@ cdef class TradingPair(Asset):
|
|||||||
object first_traded=None,
|
object first_traded=None,
|
||||||
object auto_close_date=None,
|
object auto_close_date=None,
|
||||||
object exchange_full=None,
|
object exchange_full=None,
|
||||||
object min_trade_size=None):
|
float min_trade_size=0.0001,
|
||||||
|
float max_trade_size=1000000,
|
||||||
|
float maker=0.0015,
|
||||||
|
float taker=0.0025,
|
||||||
|
float lot=0,
|
||||||
|
int decimals = 8,
|
||||||
|
int trading_state=0,
|
||||||
|
object data_source='catalyst'):
|
||||||
"""
|
"""
|
||||||
Replicates the Asset constructor with some built-in conventions
|
Replicates the Asset constructor with some built-in conventions
|
||||||
and a new 'leverage' attribute.
|
and adds properties for leverage and fees.
|
||||||
|
|
||||||
Symbol
|
Symbol
|
||||||
------
|
------
|
||||||
@@ -468,8 +491,6 @@ cdef class TradingPair(Asset):
|
|||||||
highest volume and market cap generally benefit from high leverage.
|
highest volume and market cap generally benefit from high leverage.
|
||||||
New currencies from ICO generally cannot be leveraged.
|
New currencies from ICO generally cannot be leveraged.
|
||||||
|
|
||||||
The leverage value is either None or and integer.
|
|
||||||
|
|
||||||
Leverage allows you to open a larger position with a smaller amount
|
Leverage allows you to open a larger position with a smaller amount
|
||||||
of funds. For example, if you open a $5,000 position in BTC/USD
|
of funds. For example, if you open a $5,000 position in BTC/USD
|
||||||
with 5:1 leverage, only one-fifth of this amount, or $1000, will be
|
with 5:1 leverage, only one-fifth of this amount, or $1000, will be
|
||||||
@@ -479,6 +500,11 @@ cdef class TradingPair(Asset):
|
|||||||
the position. If you open with 1:1 leverage, $5,000 of your balance
|
the position. If you open with 1:1 leverage, $5,000 of your balance
|
||||||
will be tied to the position.
|
will be tied to the position.
|
||||||
|
|
||||||
|
Fees
|
||||||
|
----
|
||||||
|
Exchanges generally charge a taker (taking from the order book) or
|
||||||
|
maker (adding to the order book) fee.
|
||||||
|
|
||||||
:param symbol:
|
:param symbol:
|
||||||
:param exchange:
|
:param exchange:
|
||||||
:param start_date:
|
:param start_date:
|
||||||
@@ -493,21 +519,23 @@ 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 taker:
|
||||||
|
:param data_source
|
||||||
|
:param decimals
|
||||||
|
:param lot
|
||||||
"""
|
"""
|
||||||
|
|
||||||
symbol = symbol.lower()
|
symbol = symbol.lower()
|
||||||
try:
|
try:
|
||||||
self.market_currency, self.base_currency = symbol.split('_')
|
self.base_currency, self.quote_currency = symbol.split('_')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise InvalidSymbolError(symbol=symbol, error=e)
|
raise InvalidSymbolError(symbol=symbol, error=e)
|
||||||
|
|
||||||
if sid == 0 or sid is None:
|
if sid == 0 or sid is None:
|
||||||
try:
|
try:
|
||||||
# sid = abs(hash(symbol)) % (10 ** 4)
|
sid = get_sid(symbol)
|
||||||
# TODO: try to encode the symbol in the main scope
|
|
||||||
sid = int(
|
|
||||||
hashlib.sha256(symbol.encode('utf-8')).hexdigest(), 16
|
|
||||||
) % 10 ** 6
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise SidHashError(symbol=symbol)
|
raise SidHashError(symbol=symbol)
|
||||||
|
|
||||||
@@ -515,11 +543,14 @@ cdef class TradingPair(Asset):
|
|||||||
asset_name = ' / '.join(symbol.split('_')).upper()
|
asset_name = ' / '.join(symbol.split('_')).upper()
|
||||||
|
|
||||||
if start_date is None:
|
if start_date is None:
|
||||||
start_date = pd.Timestamp.utcnow()
|
start_date = pd.to_datetime('2009-1-1', utc=True)
|
||||||
|
|
||||||
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,
|
||||||
@@ -530,19 +561,26 @@ cdef class TradingPair(Asset):
|
|||||||
first_traded=first_traded,
|
first_traded=first_traded,
|
||||||
auto_close_date=auto_close_date,
|
auto_close_date=auto_close_date,
|
||||||
exchange_full=exchange_full,
|
exchange_full=exchange_full,
|
||||||
min_trade_size=min_trade_size
|
min_trade_size=min_trade_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.maker = maker
|
||||||
|
self.taker = taker
|
||||||
self.leverage = leverage
|
self.leverage = leverage
|
||||||
self.end_daily = end_daily
|
self.end_daily = end_daily
|
||||||
self.end_minute = end_minute
|
self.end_minute = end_minute
|
||||||
self.exchange_symbol = exchange_symbol
|
self.exchange_symbol = exchange_symbol
|
||||||
|
self.trading_state = trading_state
|
||||||
|
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}, ' \
|
||||||
'Introduced On: {start_date}, ' \
|
'Introduced On: {start_date}, ' \
|
||||||
'Market Currency: {market_currency}, ' \
|
|
||||||
'Base Currency: {base_currency}, ' \
|
'Base Currency: {base_currency}, ' \
|
||||||
|
'Quote Currency: {quote_currency}, ' \
|
||||||
'Exchange Leverage: {leverage}, ' \
|
'Exchange Leverage: {leverage}, ' \
|
||||||
'Minimum Trade Size: {min_trade_size} ' \
|
'Minimum Trade Size: {min_trade_size} ' \
|
||||||
'Last daily ingestion: {end_daily} ' \
|
'Last daily ingestion: {end_daily} ' \
|
||||||
@@ -551,7 +589,7 @@ cdef class TradingPair(Asset):
|
|||||||
sid=self.sid,
|
sid=self.sid,
|
||||||
exchange=self.exchange,
|
exchange=self.exchange,
|
||||||
start_date=self.start_date,
|
start_date=self.start_date,
|
||||||
market_currency=self.market_currency,
|
quote_currency=self.quote_currency,
|
||||||
base_currency=self.base_currency,
|
base_currency=self.base_currency,
|
||||||
leverage=self.leverage,
|
leverage=self.leverage,
|
||||||
min_trade_size=self.min_trade_size,
|
min_trade_size=self.min_trade_size,
|
||||||
@@ -559,6 +597,18 @@ cdef class TradingPair(Asset):
|
|||||||
end_minute=self.end_minute
|
end_minute=self.end_minute
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cpdef to_dict(self):
|
||||||
|
"""
|
||||||
|
Convert to a python dict.
|
||||||
|
"""
|
||||||
|
#TODO: missing fields
|
||||||
|
super_dict = super(TradingPair, self).to_dict()
|
||||||
|
super_dict['end_daily'] = self.end_daily
|
||||||
|
super_dict['end_minute'] = self.end_minute
|
||||||
|
super_dict['leverage'] = self.leverage
|
||||||
|
super_dict['min_trade_size'] = self.min_trade_size
|
||||||
|
return super_dict
|
||||||
|
|
||||||
def is_exchange_open(self, dt_minute):
|
def is_exchange_open(self, dt_minute):
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
@@ -570,7 +620,7 @@ cdef class TradingPair(Asset):
|
|||||||
-------
|
-------
|
||||||
boolean: whether the asset's exchange is open at the given minute.
|
boolean: whether the asset's exchange is open at the given minute.
|
||||||
"""
|
"""
|
||||||
#TODO: consider implementing to spot holds
|
#TODO: make more dymanic to catch holds
|
||||||
return True
|
return True
|
||||||
|
|
||||||
cpdef __reduce__(self):
|
cpdef __reduce__(self):
|
||||||
@@ -580,6 +630,7 @@ 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,
|
||||||
@@ -590,7 +641,12 @@ 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)
|
||||||
|
|||||||
+14
-1
@@ -1,5 +1,18 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
import logbook
|
import logbook
|
||||||
|
|
||||||
LOG_LEVEL = logbook.INFO
|
''' You can override the LOG level from your environment.
|
||||||
|
For example, if you want to see the DEBUG messages, run:
|
||||||
|
$ export CATALYST_LOG_LEVEL=10
|
||||||
|
'''
|
||||||
|
LOG_LEVEL = int(os.environ.get('CATALYST_LOG_LEVEL', logbook.INFO))
|
||||||
|
|
||||||
|
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
|
||||||
|
'{exchange}/symbols.json'
|
||||||
|
|
||||||
|
DATE_TIME_FORMAT = '%Y-%m-%d %H:%M'
|
||||||
|
DATE_FORMAT = '%Y-%m-%d'
|
||||||
|
|
||||||
|
AUTO_INGEST = False
|
||||||
|
|||||||
+233
-145
@@ -1,39 +1,47 @@
|
|||||||
import json, time, csv
|
import 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 os, time, shutil, requests, logbook
|
import requests
|
||||||
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 = int(time.time())
|
DT_END = pd.to_datetime('today').value // 10 ** 9
|
||||||
CSV_OUT_FOLDER = '/var/tmp/catalyst/data/poloniex/'
|
CSV_OUT_FOLDER = os.environ.get('CSV_OUT_FOLDER', '/efs/exchanges/poloniex/')
|
||||||
CSV_OUT_FOLDER = '/Volumes/enigma/data/poloniex/'
|
CONN_RETRIES = 2
|
||||||
CONN_RETRIES = 2
|
|
||||||
|
|
||||||
logbook.StderrHandler().push_application()
|
logbook.StderrHandler().push_application()
|
||||||
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):
|
||||||
try:
|
try:
|
||||||
os.makedirs(CSV_OUT_FOLDER)
|
os.makedirs(CSV_OUT_FOLDER)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Failed to create data folder: %s' % CSV_OUT_FOLDER)
|
log.error('Failed to create data folder: {}'.format(
|
||||||
|
CSV_OUT_FOLDER))
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
'''
|
|
||||||
Retrieves and returns all currency pairs from the exchange
|
|
||||||
'''
|
|
||||||
def get_currency_pairs(self):
|
def get_currency_pairs(self):
|
||||||
|
'''
|
||||||
|
Retrieves and returns all currency pairs from the exchange
|
||||||
|
'''
|
||||||
url = self._api_path + 'command=returnTicker'
|
url = self._api_path + 'command=returnTicker'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -44,102 +52,154 @@ 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()
|
||||||
|
|
||||||
log.debug('Currency pairs retrieved successfully: %d' % (len(self.currency_pairs)))
|
log.debug('Currency pairs retrieved successfully: {}'.format(
|
||||||
|
len(self.currency_pairs)
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Helper function that reads tradeID and date fields from CSV readline
|
|
||||||
'''
|
|
||||||
def _retrieve_tradeID_date(self, row):
|
def _retrieve_tradeID_date(self, row):
|
||||||
|
'''
|
||||||
|
Helper function that reads tradeID and date fields from CSV readline
|
||||||
|
'''
|
||||||
tId = int(row.split(',')[0])
|
tId = int(row.split(',')[0])
|
||||||
d = pd.to_datetime( row.split(',')[1], infer_datetime_format=True).value // 10 ** 9
|
d = pd.to_datetime(row.split(',')[1],
|
||||||
|
infer_datetime_format=True).value // 10 ** 9
|
||||||
return tId, d
|
return tId, d
|
||||||
|
|
||||||
'''
|
def retrieve_trade_history(self, currencyPair, start=DT_START,
|
||||||
Retrieves TradeHistory from exchange for a given currencyPair between start and end dates.
|
end=DT_END, temp=None):
|
||||||
If no start date is provided, uses a system-wide one (beginning of time for cryptotrading)
|
'''
|
||||||
If no end date is provided, 'now' is used
|
Retrieves TradeHistory from exchange for a given currencyPair
|
||||||
|
between start and end dates. If no start date is provided, uses
|
||||||
|
a system-wide one (beginning of time for cryptotrading).
|
||||||
|
If no end date is provided, 'now' is used.
|
||||||
|
|
||||||
Stores results in CSV file on disk.
|
Stores results in CSV file on disk.
|
||||||
This function is called recursively to work around the limitations imposed by the provider API.
|
|
||||||
'''
|
This function is called recursively to work around the
|
||||||
def retrieve_trade_history(self, currencyPair, start=DT_START, end=DT_END, temp=None):
|
limitations imposed by the provider API.
|
||||||
|
'''
|
||||||
csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
|
csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Check what data we already have on disk, reading first and last lines from file.
|
Check what data we already have on disk, reading first and last
|
||||||
Data is stored on file from NEWEST to OLDEST.
|
lines from file. Data is stored on file from NEWEST to OLDEST.
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
with open(csv_fn, 'ab+') as f:
|
with open(csv_fn, 'ab+') as f:
|
||||||
f.seek(0, os.SEEK_END)
|
f.seek(0, os.SEEK_END)
|
||||||
if(f.tell() > 2): # First check file is not zero size
|
if(f.tell() > 2): # Check file size is not 0
|
||||||
f.seek(0) # Go to the beginning to read first line
|
f.seek(0) # Go to start to read
|
||||||
last_tradeID, end_file = self._retrieve_tradeID_date(f.readline())
|
last_tradeID, end_file = self._retrieve_tradeID_date(
|
||||||
f.seek(-2, os.SEEK_END) # Jump to the second last byte.
|
f.readline())
|
||||||
while f.read(1) != b"\n": # Until EOL is found...
|
f.seek(-2, os.SEEK_END) # Jump to the 2nd last byte
|
||||||
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
|
while f.read(1) != b"\n": # Until EOL is found...
|
||||||
first_tradeID, start_file = self._retrieve_tradeID_date(f.readline())
|
# ...jump back the read byte plus one more.
|
||||||
|
f.seek(-2, os.SEEK_CUR)
|
||||||
|
first_tradeID, start_file = self._retrieve_tradeID_date(
|
||||||
|
f.readline())
|
||||||
|
|
||||||
if( first_tradeID == 1 and end_file + 3600 > DT_END ):
|
if(end_file + 3600 * 6 > DT_END
|
||||||
|
and (first_tradeID == 1
|
||||||
|
or (currencyPair == 'BTC_HUC'
|
||||||
|
and first_tradeID == 2)
|
||||||
|
or (currencyPair == 'BTC_RIC'
|
||||||
|
and first_tradeID == 2)
|
||||||
|
or (currencyPair == 'BTC_XCP'
|
||||||
|
and first_tradeID == 2)
|
||||||
|
or (currencyPair == 'BTC_NAV'
|
||||||
|
and first_tradeID == 4569)
|
||||||
|
or (currencyPair == 'BTC_POT'
|
||||||
|
and first_tradeID == 23511))):
|
||||||
return
|
return
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Error opening file: %s' % csv_fn)
|
log.error('Error opening file: {}'.format(csv_fn))
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Poloniex API limits querying TradeHistory to intervals smaller than 1 month,
|
Poloniex API limits querying TradeHistory to intervals smaller
|
||||||
so we make sure that start date is never more than 1 month apart from end date
|
than 1 month, so we make sure that start date is never more than
|
||||||
|
1 month apart from end date
|
||||||
'''
|
'''
|
||||||
if( end - start > 2419200 ): # 60 s/min * 60 min/hr * 24 hr/day * 28 days
|
if(end - start > 2419200): # 60s/min * 60min/hr * 24hr/day * 28days
|
||||||
newstart = end - 2419200
|
newstart = end - 2419200
|
||||||
else:
|
else:
|
||||||
newstart = start
|
newstart = start
|
||||||
|
|
||||||
log.debug(currencyPair+': Retrieving from '+str(newstart)+' to '+str(end) +'\t '
|
log.debug('{}: Retrieving from {} to {}\t {} - {}'.format(
|
||||||
+ time.ctime(newstart) + ' - '+ time.ctime(end))
|
currencyPair, str(newstart), str(end),
|
||||||
|
time.ctime(newstart), time.ctime(end)))
|
||||||
|
|
||||||
url = self._api_path + 'command=returnTradeHistory¤cyPair=' + currencyPair + '&start=' + str(newstart) + '&end=' + str(end)
|
url = '{path}command=returnTradeHistory¤cyPair={pair}' \
|
||||||
|
'&start={start}&end={end}'.format(
|
||||||
|
path=self._api_path,
|
||||||
|
pair=currencyPair,
|
||||||
|
start=str(newstart),
|
||||||
|
end=str(end)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
attempts = 0
|
||||||
response = requests.get(url)
|
success = 0
|
||||||
except Exception as e:
|
while attempts < CONN_RETRIES:
|
||||||
log.error('Failed to retrieve trade history data for %s' % currencyPair)
|
try:
|
||||||
log.exception(e)
|
response = requests.get(url)
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Failed to retrieve trade history data'
|
||||||
|
'for {}'.format(currencyPair))
|
||||||
|
log.exception(e)
|
||||||
|
attempts += 1
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if(isinstance(response.json(), dict)
|
||||||
|
and response.json()['error']):
|
||||||
|
log.error('Failed to to retrieve trade history data '
|
||||||
|
'for {}: {}'.format(
|
||||||
|
currencyPair,
|
||||||
|
response.json()['error']
|
||||||
|
))
|
||||||
|
attempts += 1
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
attempts += 1
|
||||||
|
else:
|
||||||
|
success = 1
|
||||||
|
break
|
||||||
|
|
||||||
|
if not success:
|
||||||
return None
|
return None
|
||||||
else:
|
|
||||||
if isinstance(response.json(), dict) and response.json()['error']:
|
|
||||||
log.error('Failed to to retrieve trade history data for %s: %s' % (currencyPair,response.json()['error']))
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
If we get to transactionId == 1, and we already have that on disk,
|
If we get to transactionId == 1, and we already have that on
|
||||||
we got to the end of TradeHistory for this coin.
|
disk, we got to the end of TradeHistory for this coin.
|
||||||
'''
|
'''
|
||||||
if('first_tradeID' in locals() and response.json()[-1]['tradeID'] == first_tradeID):
|
if('first_tradeID' in locals()
|
||||||
|
and response.json()[-1]['tradeID'] == first_tradeID):
|
||||||
return
|
return
|
||||||
|
|
||||||
'''
|
'''
|
||||||
There are primarily two scenarios:
|
There are primarily two scenarios:
|
||||||
a) There is newer data available that we need to add at the beginning
|
a) There is newer data available that we need to add at
|
||||||
of the file. We'll retrieve all what we need until we get to what
|
the beginning of the file. We'll retrieve all what we
|
||||||
we already have, writing it to a temporary file; and we will write
|
need until we get to what we already have, writing it
|
||||||
that at the beginning of our existing file.
|
to a temporary file; and we will write that at the
|
||||||
b) We are going back in time, appending at the end of our existing
|
beginning of our existing file.
|
||||||
TradeHistory until the first transaction for this currencyPair
|
b) We are going back in time, appending at the end of
|
||||||
|
our existing TradeHistory until the first transaction
|
||||||
|
for this currencyPair
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
if( 'end_file' in locals() and end_file + 3600 < end):
|
if(temp is not None
|
||||||
|
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'],
|
||||||
@@ -148,24 +208,28 @@ 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'], infer_datetime_format=True).value // 10 ** 9
|
end = pd.to_datetime(response.json()[-1]['date'],
|
||||||
self.retrieve_trade_history(currencyPair, start, end, temp=temp)
|
infer_datetime_format=True
|
||||||
|
).value // 10**9
|
||||||
|
self.retrieve_trade_history(currencyPair, start,
|
||||||
|
end, temp=temp)
|
||||||
else:
|
else:
|
||||||
with open(csv_fn,'rb+') as f:
|
with open(csv_fn, 'rb+') as f:
|
||||||
shutil.copyfileobj(f,temp)
|
shutil.copyfileobj(f, temp)
|
||||||
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() and item['tradeID'] >= first_tradeID ):
|
if('first_tradeID' in locals()
|
||||||
|
and item['tradeID'] >= first_tradeID):
|
||||||
continue
|
continue
|
||||||
csvwriter.writerow([
|
csvwriter.writerow([
|
||||||
item['tradeID'],
|
item['tradeID'],
|
||||||
@@ -176,84 +240,104 @@ class PoloniexCurator(object):
|
|||||||
item['total'],
|
item['total'],
|
||||||
item['globalTradeID']
|
item['globalTradeID']
|
||||||
])
|
])
|
||||||
end = pd.to_datetime( response.json()[-1]['date'], infer_datetime_format=True).value // 10 ** 9
|
end = pd.to_datetime(response.json()[-1]['date'],
|
||||||
|
infer_datetime_format=True).value//10**9
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Error opening %s' % csv_fn)
|
log.error('Error opening {}'.format(csv_fn))
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
If we got here, we aren't done yet. Call recursively with 'end' times
|
If we got here, we aren't done yet. Call recursively with
|
||||||
that go sequentially back in time.
|
'end' times that go sequentially back in time.
|
||||||
'''
|
'''
|
||||||
self.retrieve_trade_history(currencyPair, start, end)
|
self.retrieve_trade_history(currencyPair, start, end)
|
||||||
|
|
||||||
|
def generate_ohlcv(self, df):
|
||||||
'''
|
'''
|
||||||
Generates OHLCV dataframe from a dataframe containing all TradeHistory
|
Generates OHLCV dataframe from a dataframe containing all TradeHistory
|
||||||
by resampling with 1-minute period
|
by resampling with 1-minute period
|
||||||
'''
|
'''
|
||||||
def generate_ohlcv(self, df):
|
df.set_index('date', inplace=True) # Index by date
|
||||||
df.set_index('date', inplace=True) # Index by date
|
vol = df['total'].to_frame('volume') # set Vol aside
|
||||||
vol = df['total'].to_frame('volume') # Will deal with vol separately, as ohlc() messes it up
|
df.drop('total', axis=1, inplace=True) # Drop volume data
|
||||||
df.drop('total', axis=1, inplace=True) # Drop volume data from dataframe
|
ohlc = df.resample('T').ohlc() # Resample OHLC 1min
|
||||||
ohlc = df.resample('T').ohlc() # Resample OHLC in 1min bins
|
ohlc.columns = ohlc.columns.map(lambda t: t[1]) # 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 forward 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 + Volume
|
|
||||||
return ohlcv
|
return ohlcv
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Generates OHLCV data file with 1minute bars from TradeHistory on disk
|
|
||||||
'''
|
|
||||||
def write_ohlcv_file(self, currencyPair):
|
def write_ohlcv_file(self, currencyPair):
|
||||||
|
'''
|
||||||
|
Generates OHLCV data file with 1minute bars from TradeHistory on disk
|
||||||
|
'''
|
||||||
csv_trades = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
|
csv_trades = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
|
||||||
csv_1min = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
|
csv_1min = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
|
||||||
#if( os.path.isfile(csv_1min) ):
|
if(os.path.getmtime(csv_1min) > time.time() - 7200):
|
||||||
# log.debug(currencyPair+': 1min data already present. Delete the file if you want to rebuild it.')
|
log.debug(currencyPair+': 1min data file already up to date. '
|
||||||
#else:
|
'Delete the file if you want to rebuild it.')
|
||||||
df = pd.read_csv(csv_trades, names=['tradeID','date','type','rate','amount','total','globalTradeID'],
|
else:
|
||||||
dtype = {'tradeID': int, 'date': str, 'type': str, 'rate': float, 'amount': float, 'total': float, 'globalTradeID': int } )
|
df = pd.read_csv(csv_trades,
|
||||||
df.drop(['tradeID','type','amount','globalTradeID'], axis=1, inplace=True)
|
names=['tradeID',
|
||||||
df['date'] = pd.to_datetime(df['date'], infer_datetime_format=True)
|
'date',
|
||||||
ohlcv = self.generate_ohlcv(df)
|
'type',
|
||||||
try:
|
'rate',
|
||||||
with open(csv_1min, 'w') as csvfile:
|
'amount',
|
||||||
csvwriter = csv.writer(csvfile)
|
'total',
|
||||||
for item in ohlcv.itertuples():
|
'globalTradeID'],
|
||||||
if item.Index == 0:
|
dtype={'tradeID': int,
|
||||||
continue
|
'date': str,
|
||||||
csvwriter.writerow([
|
'type': str,
|
||||||
item.Index.value // 10 ** 9,
|
'rate': float,
|
||||||
item.open,
|
'amount': float,
|
||||||
item.high,
|
'total': float,
|
||||||
item.low,
|
'globalTradeID': int}
|
||||||
item.close,
|
)
|
||||||
item.volume,
|
df.drop(['tradeID', 'type', 'amount', 'globalTradeID'],
|
||||||
])
|
axis=1, inplace=True)
|
||||||
except Exception as e:
|
df['date'] = pd.to_datetime(df['date'], infer_datetime_format=True)
|
||||||
log.error('Error opening %s' % csv_fn)
|
ohlcv = self.generate_ohlcv(df)
|
||||||
log.exception(e)
|
try:
|
||||||
log.debug(currencyPair+': Generated 1min OHLCV data.')
|
with open(csv_1min, 'w') as csvfile:
|
||||||
|
csvwriter = csv.writer(csvfile)
|
||||||
|
for item in ohlcv.itertuples():
|
||||||
|
if item.Index == 0:
|
||||||
|
continue
|
||||||
|
csvwriter.writerow([
|
||||||
|
item.Index.value // 10 ** 9,
|
||||||
|
item.open,
|
||||||
|
item.high,
|
||||||
|
item.low,
|
||||||
|
item.close,
|
||||||
|
item.volume,
|
||||||
|
])
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Error opening {}'.format(csv_1min))
|
||||||
|
log.exception(e)
|
||||||
|
log.debug('{}: Generated 1min OHLCV data.'.format(currencyPair))
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Returns a data frame for a given currencyPair from data on disk
|
|
||||||
'''
|
|
||||||
def onemin_to_dataframe(self, currencyPair, start, end):
|
def onemin_to_dataframe(self, currencyPair, start, end):
|
||||||
csv_fn = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
|
'''
|
||||||
df = pd.read_csv(csv_fn, names=['date', 'open', 'high', 'low', 'close', 'volume'])
|
Returns a data frame for a given currencyPair from data on disk
|
||||||
df['date'] = pd.to_datetime(df['date'],unit='s')
|
'''
|
||||||
|
csv_fn = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
|
||||||
|
df = pd.read_csv(csv_fn, names=['date',
|
||||||
|
'open',
|
||||||
|
'high',
|
||||||
|
'low',
|
||||||
|
'close',
|
||||||
|
'volume'])
|
||||||
|
df['date'] = pd.to_datetime(df['date'], unit='s')
|
||||||
df.set_index('date', inplace=True)
|
df.set_index('date', inplace=True)
|
||||||
return df[start : end]
|
return df[start:end]
|
||||||
|
|
||||||
'''
|
|
||||||
Generates a symbols.json file with corresponding start_date for each currencyPair
|
|
||||||
'''
|
|
||||||
def generate_symbols_json(self, filename=None):
|
def generate_symbols_json(self, filename=None):
|
||||||
|
'''
|
||||||
|
Generates a symbols.json file with corresponding start_date
|
||||||
|
for each currencyPair
|
||||||
|
'''
|
||||||
symbol_map = {}
|
symbol_map = {}
|
||||||
|
|
||||||
if(filename is None):
|
if(filename is None):
|
||||||
@@ -262,33 +346,37 @@ class PoloniexCurator(object):
|
|||||||
with open(filename, 'w') as symbols:
|
with open(filename, 'w') as symbols:
|
||||||
for currencyPair in self.currency_pairs:
|
for currencyPair in self.currency_pairs:
|
||||||
start = None
|
start = None
|
||||||
csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
|
csv_fn = '{}crypto_trades-{}.csv'.format(
|
||||||
|
CSV_OUT_FOLDER,
|
||||||
|
currencyPair)
|
||||||
with open(csv_fn, 'r') as f:
|
with open(csv_fn, 'r') as f:
|
||||||
f.seek(0, os.SEEK_END)
|
f.seek(0, os.SEEK_END)
|
||||||
if(f.tell() > 2): # First check file is not zero size
|
if(f.tell() > 2): # Check file size is not 0
|
||||||
f.seek(-2, os.SEEK_END) # Jump to the second last byte.
|
f.seek(-2, os.SEEK_END) # Jump to 2nd last byte
|
||||||
while f.read(1) != b"\n": # Until EOL is found...
|
while f.read(1) != b"\n": # Until EOL is found...
|
||||||
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
|
# ...jump back the read byte plus one more.
|
||||||
start = pd.to_datetime( f.readline().split(',')[1], infer_datetime_format=True)
|
f.seek(-2, os.SEEK_CUR)
|
||||||
|
start = pd.to_datetime(f.readline().split(',')[1],
|
||||||
|
infer_datetime_format=True)
|
||||||
|
|
||||||
if(start is None):
|
if(start is None):
|
||||||
start = time.gmtime()
|
start = time.gmtime()
|
||||||
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, separators=(',',':'))
|
json.dump(symbol_map, symbols, sort_keys=True, indent=2,
|
||||||
|
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))
|
||||||
pc.write_ohlcv_file(currencyPair)
|
pc.write_ohlcv_file(currencyPair)
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
# 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,10 +13,9 @@
|
|||||||
# 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 time, sleep
|
from time import sleep
|
||||||
|
|
||||||
from abc import abstractmethod, abstractproperty
|
from abc import abstractmethod, abstractproperty
|
||||||
import logbook
|
import logbook
|
||||||
@@ -37,6 +36,7 @@ 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 and ingestion of bundle.
|
# User has instructed local compilation & 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 to
|
# Post-process metadata using cached symbol frames, and write
|
||||||
# disk. This metadata must be written before any attempt to write
|
# to disk. This metadata must be written before any attempt
|
||||||
# minute data.
|
# to write minute data.
|
||||||
metadata = self._post_process_metadata(
|
metadata = self._post_process_metadata(
|
||||||
raw_metadata,
|
raw_metadata,
|
||||||
cache,
|
cache,
|
||||||
@@ -184,10 +184,11 @@ class BaseBundle(object):
|
|||||||
show_progress=show_progress,
|
show_progress=show_progress,
|
||||||
)
|
)
|
||||||
|
|
||||||
# For legacy purposes, this call is required to ensure the database
|
# For legacy purposes, this call is required to ensure the
|
||||||
# contains an appropriately initialized file structure. We don't
|
# database contains an appropriately initialized file
|
||||||
# forsee a usecase for adjustments at this time, but may later
|
# structure. We don't forsee a usecase for adjustments at
|
||||||
# choose to expose this functionality in the future.
|
# this time, but may later choose to expose this functionality
|
||||||
|
# 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)
|
||||||
@@ -232,11 +233,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)
|
||||||
@@ -269,10 +270,10 @@ class BaseBundle(object):
|
|||||||
page_number,
|
page_number,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
except ValueError as e:
|
except ValueError:
|
||||||
raw = pd.DataFrame([])
|
raw = pd.DataFrame([])
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception:
|
||||||
log.exception(
|
log.exception(
|
||||||
'Failed to load metadata from {}. '
|
'Failed to load metadata from {}. '
|
||||||
'Retrying.'.format(self.name)
|
'Retrying.'.format(self.name)
|
||||||
@@ -283,7 +284,6 @@ 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 entry
|
# Attempt to load data from disk, the cache should have an
|
||||||
# for each symbol at this point of the execution. If one does
|
# entry for each symbol at this point of the execution. If one
|
||||||
# not exist, we should fail.
|
# does 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: {0}'.format(symbol)
|
'Unable to find cached data for 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 being
|
# next symbol. If the raw_data is updated, it is cached before
|
||||||
# returned.
|
# being 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,7 +468,6 @@ 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[
|
||||||
@@ -482,7 +481,7 @@ class BaseBundle(object):
|
|||||||
|
|
||||||
return raw_data
|
return raw_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
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,6 +16,7 @@
|
|||||||
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):
|
||||||
@@ -38,6 +39,7 @@ class BasePricingBundle(BaseBundle):
|
|||||||
('volume', 'float64'),
|
('volume', 'float64'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class BaseCryptoPricingBundle(BasePricingBundle):
|
class BaseCryptoPricingBundle(BasePricingBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def calendar_name(self):
|
def calendar_name(self):
|
||||||
@@ -55,6 +57,7 @@ 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,6 +37,7 @@ 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),
|
||||||
@@ -135,6 +136,7 @@ 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
|
||||||
@@ -705,4 +707,5 @@ def _make_bundle_core():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
bundles, register_bundle, register, unregister, ingest, load, clean = _make_bundle_core()
|
bundles, register_bundle, register, unregister, ingest, load, clean = \
|
||||||
|
_make_bundle_core()
|
||||||
|
|||||||
@@ -14,19 +14,17 @@
|
|||||||
# 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):
|
||||||
@@ -46,7 +44,8 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
@lazyval
|
@lazyval
|
||||||
def tar_url(self):
|
def tar_url(self):
|
||||||
return (
|
return (
|
||||||
'https://s3.amazonaws.com/enigmaco/catalyst-bundles/poloniex/poloniex-bundle.tar.gz'
|
'https://s3.amazonaws.com/enigmaco/catalyst-bundles/'
|
||||||
|
'poloniex/poloniex-bundle.tar.gz'
|
||||||
)
|
)
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
@@ -67,12 +66,11 @@ 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):
|
||||||
@@ -98,7 +96,8 @@ 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 calculate the number of bars
|
# The end date and frequency should be used to
|
||||||
|
# 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)
|
||||||
@@ -116,8 +115,9 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
)
|
)
|
||||||
raw.set_index('date', inplace=True)
|
raw.set_index('date', inplace=True)
|
||||||
|
|
||||||
# BcolzDailyBarReader introduces a 1/1000 factor in the way pricing is stored
|
# BcolzDailyBarReader introduces a 1/1000 factor in the way
|
||||||
# on disk, which we compensate here to get the right pricing amounts
|
# pricing is stored on disk, which we compensate here to get
|
||||||
|
# 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,7 +139,6 @@ 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,
|
||||||
@@ -171,6 +170,7 @@ 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,9 +180,7 @@ 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,7 +16,6 @@
|
|||||||
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
|
||||||
@@ -26,25 +25,16 @@ 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):
|
||||||
@@ -109,8 +99,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
|
# cut out all the other stuff in the name column. We need to
|
||||||
# we need to escape the paren because it is actually splitting on a regex
|
# 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
|
||||||
@@ -175,7 +165,6 @@ 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]})
|
||||||
@@ -186,7 +175,6 @@ 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.
|
||||||
"""
|
"""
|
||||||
@@ -200,10 +188,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?' + urlencode(query_params)
|
'https://www.quandl.com/api/v3/datasets.csv?'
|
||||||
|
+ urlencode(query_params)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _format_wiki_url(self,
|
def _format_wiki_url(self,
|
||||||
api_key,
|
api_key,
|
||||||
symbol,
|
symbol,
|
||||||
@@ -229,5 +217,6 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@@ -88,11 +88,11 @@ class AssetDispatchBarReader(with_metaclass(ABCMeta)):
|
|||||||
if self._last_available_dt is not None:
|
if self._last_available_dt is not None:
|
||||||
return self._last_available_dt
|
return self._last_available_dt
|
||||||
else:
|
else:
|
||||||
return min(r.last_available_dt for r in self._readers.values())
|
return min(r.last_available_dt for r in list(self._readers.values()))
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
def first_trading_day(self):
|
def first_trading_day(self):
|
||||||
return max(r.first_trading_day for r in self._readers.values())
|
return max(r.first_trading_day for r in list(self._readers.values()))
|
||||||
|
|
||||||
def get_value(self, sid, dt, field):
|
def get_value(self, sid, dt, field):
|
||||||
asset = self._asset_finder.retrieve_asset(sid)
|
asset = self._asset_finder.retrieve_asset(sid)
|
||||||
@@ -133,11 +133,13 @@ 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):
|
||||||
|
|||||||
+28
-84
@@ -12,7 +12,6 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import datetime
|
|
||||||
import os
|
import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
@@ -23,6 +22,7 @@ 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,8 +32,6 @@ 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
|
||||||
@@ -103,7 +101,7 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
|
|||||||
trading_day = get_calendar('OPEN').trading_day
|
trading_day = get_calendar('OPEN').trading_day
|
||||||
|
|
||||||
# TODO: consider making configurable
|
# TODO: consider making configurable
|
||||||
bm_symbol = 'btc_usdt'
|
bm_symbol = 'btc_usd'
|
||||||
# if trading_days is None:
|
# if trading_days is None:
|
||||||
# trading_days = get_calendar('OPEN').schedule
|
# trading_days = get_calendar('OPEN').schedule
|
||||||
|
|
||||||
@@ -129,11 +127,13 @@ 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 the last
|
# If we are using the bundle to retrieve the cryptobenchmark, find
|
||||||
# date for which there is trading data in the bundle
|
# the last date for which there is trading data in the bundle
|
||||||
asset = bundle_data.asset_finder.lookup_symbol(symbol=bm_symbol,as_of_date=None)
|
asset = bundle_data.asset_finder.lookup_symbol(
|
||||||
|
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(bundle_data.daily_bar_reader._spot_col('day')[ix],unit='s')
|
last_date = pd.to_datetime(
|
||||||
|
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,27 +142,31 @@ 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.poloniex.poloniex import Poloniex
|
from catalyst.exchange.utils.factory import get_exchange
|
||||||
exchange = Poloniex('', '', '')
|
exchange = get_exchange(
|
||||||
|
exchange_name='bitfinex', base_currency='usd'
|
||||||
|
)
|
||||||
|
exchange.init()
|
||||||
|
|
||||||
benchmark_asset = exchange.get_asset(bm_symbol)
|
benchmark_asset = exchange.get_asset(bm_symbol)
|
||||||
|
|
||||||
# exchange.get_history_window() already ensures that we have the right data
|
# exchange.get_history_window() already ensures that we have the right data
|
||||||
# for the right dates
|
# for the right dates
|
||||||
br = exchange.get_history_window(
|
br = exchange.get_history_window_with_bundle(
|
||||||
assets=[benchmark_asset],
|
assets=[benchmark_asset],
|
||||||
end_dt=last_date,
|
end_dt=last_date,
|
||||||
bar_count=pd.Timedelta(last_date - start_dt).days,
|
bar_count=pd.Timedelta(last_date - start_dt).days,
|
||||||
frequency='1d',
|
frequency='1d',
|
||||||
field='close',
|
field='close',
|
||||||
data_frequency='daily')
|
data_frequency='daily',
|
||||||
|
force_auto_ingest=True)
|
||||||
br.columns = ['close']
|
br.columns = ['close']
|
||||||
br = br.pct_change(1).iloc[1:]
|
br = br.pct_change(1).iloc[1:]
|
||||||
br.loc[start_dt] = 0
|
br.loc[start_dt] = 0
|
||||||
br = br.sort_index()
|
br = br.sort_index()
|
||||||
|
|
||||||
# Override first_date for treasury data since we have it for many more years
|
# Override first_date for treasury data since we have it for many more
|
||||||
# and is independent of crypto data
|
# years 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,
|
||||||
@@ -298,14 +302,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 bundle
|
If we're using the Poloniex bundle, we'll get the benchmark from the
|
||||||
instead of downloading it from Poloniex every time we need it.
|
bundle instead of downloading it from Poloniex every time we need it.
|
||||||
Poloniex has a captcha for API queries originating from outside the US that
|
Poloniex has a captcha for API queries originating from outside the US
|
||||||
prevents users abroad from getting Catalyst to work
|
that prevents users abroad from getting Catalyst to work
|
||||||
'''
|
'''
|
||||||
logger.info(
|
logger.info(
|
||||||
(
|
('Retrieving benchmark data from bundle for {symbol!r}'
|
||||||
'Retrieving benchmark data from bundle for {symbol!r} from {first_date} to {last_date}'),
|
' 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,
|
||||||
@@ -328,10 +332,11 @@ 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 other bundles.
|
# Leaving this code here to be repurposed in the future for
|
||||||
|
# other bundles.
|
||||||
logger.info(
|
logger.info(
|
||||||
(
|
('Downloading benchmark data for {symbol!r}'
|
||||||
'Downloading benchmark data for {symbol!r} from {first_date} to {last_date}'),
|
' 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')
|
||||||
@@ -428,67 +433,6 @@ 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,12 +341,10 @@ 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': (market_opens.values.astype('datetime64[m]').
|
||||||
market_opens.values.astype('datetime64[m]').
|
astype(np.int64).tolist()),
|
||||||
astype(np.int64).tolist()),
|
'market_closes': (market_closes.values.astype('datetime64[m]').
|
||||||
'market_closes': (
|
astype(np.int64).tolist()),
|
||||||
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)
|
||||||
@@ -1256,8 +1254,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_slice = np.s_[excl_start - start_idx:excl_stop
|
||||||
excl_start - start_idx:excl_stop - start_idx + 1]
|
- start_idx + 1]
|
||||||
values = np.delete(values, excl_slice)
|
values = np.delete(values, excl_slice)
|
||||||
|
|
||||||
where = values != 0
|
where = values != 0
|
||||||
@@ -1320,9 +1318,8 @@ 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 \
|
self._complib = complib if complib is not None else self._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 to have division of ints yield float
|
from __future__ import division # Python2 req for division of ints yield float
|
||||||
|
|
||||||
from errno import ENOENT
|
from errno import ENOENT
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -120,7 +120,8 @@ 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
|
||||||
|
|
||||||
PRICE_ADJUSTMENT_FACTOR = 1000000000 # Provides 9 decimals resolution. Also affects _equities.pyx L220
|
# 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):
|
||||||
@@ -130,6 +131,7 @@ 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(
|
||||||
@@ -322,8 +324,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,11 +441,13 @@ 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)] * PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
processed = (raw_data[list(OHLC)]
|
||||||
|
* 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 * PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
processed['volume'] = (raw_data.volume
|
||||||
|
* PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
||||||
return ctable.fromdataframe(processed)
|
return ctable.fromdataframe(processed)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
An overview of most of the trading strategies in this folder can be found in the
|
||||||
|
`Examples Algorithms <https://enigmampc.github.io/catalyst/example-algos.html>`_
|
||||||
|
section of our documentation website.
|
||||||
@@ -6,7 +6,7 @@ from catalyst.api import (
|
|||||||
symbol,
|
symbol,
|
||||||
get_open_orders
|
get_open_orders
|
||||||
)
|
)
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats
|
from catalyst.exchange.utils.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'
|
||||||
@@ -83,15 +83,15 @@ def place_orders(context, amount, buying_price, selling_price, action):
|
|||||||
else:
|
else:
|
||||||
raise ValueError('invalid order action')
|
raise ValueError('invalid order action')
|
||||||
|
|
||||||
base_currency = enter_exchange.base_currency
|
quote_currency = enter_exchange.quote_currency
|
||||||
base_currency_amount = enter_exchange.portfolio.cash
|
quote_currency_amount = enter_exchange.portfolio.cash
|
||||||
|
|
||||||
exit_balances = exit_exchange.get_balances()
|
exit_balances = exit_exchange.get_balances()
|
||||||
exit_currency = context.trading_pairs[
|
exit_currency = context.trading_pairs[
|
||||||
context.selling_exchange].market_currency
|
context.selling_exchange].quote_currency
|
||||||
|
|
||||||
if exit_currency in exit_balances:
|
if exit_currency in exit_balances:
|
||||||
market_currency_amount = exit_balances[exit_currency]
|
quote_currency_amount = exit_balances[exit_currency]
|
||||||
else:
|
else:
|
||||||
log.warn(
|
log.warn(
|
||||||
'the selling exchange {exchange_name} does not hold '
|
'the selling exchange {exchange_name} does not hold '
|
||||||
@@ -102,25 +102,25 @@ def place_orders(context, amount, buying_price, selling_price, action):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if base_currency_amount < (amount * entry_price):
|
if quote_currency_amount < (amount * entry_price):
|
||||||
adj_amount = base_currency_amount / entry_price
|
adj_amount = quote_currency_amount / entry_price
|
||||||
log.warn(
|
log.warn(
|
||||||
'not enough {base_currency} ({base_currency_amount}) to buy '
|
'not enough {quote_currency} ({quote_currency_amount}) to buy '
|
||||||
'{amount}, adjusting the amount to {adj_amount}'.format(
|
'{amount}, adjusting the amount to {adj_amount}'.format(
|
||||||
base_currency=base_currency,
|
quote_currency=quote_currency,
|
||||||
base_currency_amount=base_currency_amount,
|
quote_currency_amount=quote_currency_amount,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
adj_amount=adj_amount
|
adj_amount=adj_amount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
amount = adj_amount
|
amount = adj_amount
|
||||||
|
|
||||||
elif market_currency_amount < amount:
|
elif quote_currency_amount < amount:
|
||||||
log.warn(
|
log.warn(
|
||||||
'not enough {currency} ({currency_amount}) to sell '
|
'not enough {currency} ({currency_amount}) to sell '
|
||||||
'{amount}, aborting'.format(
|
'{amount}, aborting'.format(
|
||||||
currency=exit_currency,
|
currency=exit_currency,
|
||||||
currency_amount=market_currency_amount,
|
currency_amount=quote_currency_amount,
|
||||||
amount=amount
|
amount=amount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -263,13 +263,20 @@ def analyze(context, stats):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
run_algorithm(
|
if __name__ == '__main__':
|
||||||
initialize=initialize,
|
# The execution mode: backtest or live
|
||||||
handle_data=handle_data,
|
MODE = 'live'
|
||||||
analyze=analyze,
|
if MODE == 'live':
|
||||||
exchange_name='poloniex,bitfinex',
|
run_algorithm(
|
||||||
live=True,
|
capital_base=0.1,
|
||||||
algo_namespace=algo_namespace,
|
initialize=initialize,
|
||||||
base_currency='btc',
|
handle_data=handle_data,
|
||||||
live_graph=False
|
analyze=analyze,
|
||||||
)
|
exchange_name='poloniex,bitfinex',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=algo_namespace,
|
||||||
|
base_currency='btc',
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=True,
|
||||||
|
stats_output=None,
|
||||||
|
)
|
||||||
|
|||||||
@@ -14,30 +14,25 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import (order_target_value, symbol, record,
|
||||||
|
cancel_order, get_open_orders, )
|
||||||
|
|
||||||
from catalyst.api import (
|
|
||||||
order_target_value,
|
|
||||||
symbol,
|
|
||||||
record,
|
|
||||||
cancel_order,
|
|
||||||
get_open_orders,
|
|
||||||
)
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.ASSET_NAME = 'BTC_USDT'
|
context.ASSET_NAME = 'btc_usdt'
|
||||||
context.TARGET_HODL_RATIO = 0.8
|
context.TARGET_HODL_RATIO = 0.8
|
||||||
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
|
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
|
||||||
|
|
||||||
# For all trading pairs in the poloniex bundle, the default denomination
|
|
||||||
# currently supported by Catalyst is 1/1000th of a full coin. Use this
|
|
||||||
# constant to scale the price of up to that of a full coin if desired.
|
|
||||||
context.TICK_SIZE = 1000.0
|
|
||||||
|
|
||||||
context.is_buying = True
|
context.is_buying = True
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
context.i = 0
|
context.i = 0
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
context.i += 1
|
context.i += 1
|
||||||
|
|
||||||
@@ -60,12 +55,12 @@ def handle_data(context, data):
|
|||||||
|
|
||||||
# Check if still buying and could (approximately) afford another purchase
|
# Check if still buying and could (approximately) afford another purchase
|
||||||
if context.is_buying and cash > price:
|
if context.is_buying and cash > price:
|
||||||
|
print('buying')
|
||||||
# Place order to make position in asset equal to target_hodl_value
|
# Place order to make position in asset equal to target_hodl_value
|
||||||
order_target_value(
|
order_target_value(
|
||||||
context.asset,
|
context.asset,
|
||||||
target_hodl_value,
|
target_hodl_value,
|
||||||
limit_price=price*1.1,
|
limit_price=price * 1.1,
|
||||||
stop_price=price*0.9,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
record(
|
record(
|
||||||
@@ -76,28 +71,29 @@ def handle_data(context, data):
|
|||||||
leverage=context.account.leverage,
|
leverage=context.account.leverage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def analyze(context=None, results=None):
|
def analyze(context=None, results=None):
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
# Plot the portfolio and asset data.
|
# Plot the portfolio and asset data.
|
||||||
ax1 = plt.subplot(611)
|
ax1 = plt.subplot(611)
|
||||||
results[['portfolio_value']].plot(ax=ax1)
|
results[['portfolio_value']].plot(ax=ax1)
|
||||||
ax1.set_ylabel('Portfolio Value (USD)')
|
ax1.set_ylabel('Portfolio\nValue\n(USD)')
|
||||||
|
|
||||||
ax2 = plt.subplot(612, sharex=ax1)
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
|
ax2.set_ylabel('{asset}\n(USD)'.format(asset=context.ASSET_NAME))
|
||||||
(context.TICK_SIZE * results[['price']]).plot(ax=ax2)
|
results[['price']].plot(ax=ax2)
|
||||||
|
|
||||||
trans = results.ix[[t != [] for t in results.transactions]]
|
trans = results.ix[[t != [] for t in results.transactions]]
|
||||||
buys = trans.ix[
|
buys = trans.ix[
|
||||||
[t[0]['amount'] > 0 for t in trans.transactions]
|
[t[0]['amount'] > 0 for t in trans.transactions]
|
||||||
]
|
]
|
||||||
ax2.plot(
|
ax2.scatter(
|
||||||
buys.index,
|
buys.index.to_pydatetime(),
|
||||||
context.TICK_SIZE * results.price[buys.index],
|
results.price[buys.index],
|
||||||
'^',
|
marker='^',
|
||||||
markersize=10,
|
s=100,
|
||||||
color='g',
|
c='g',
|
||||||
|
label=''
|
||||||
)
|
)
|
||||||
|
|
||||||
ax3 = plt.subplot(613, sharex=ax1)
|
ax3 = plt.subplot(613, sharex=ax1)
|
||||||
@@ -124,14 +120,29 @@ def analyze(context=None, results=None):
|
|||||||
'algorithm',
|
'algorithm',
|
||||||
'benchmark',
|
'benchmark',
|
||||||
]].plot(ax=ax5)
|
]].plot(ax=ax5)
|
||||||
ax5.set_ylabel('Percent Change')
|
ax5.set_ylabel('Percent\nChange')
|
||||||
|
|
||||||
ax6 = plt.subplot(616, sharex=ax1)
|
ax6 = plt.subplot(616, sharex=ax1)
|
||||||
results[['volume']].plot(ax=ax6)
|
results[['volume']].plot(ax=ax6)
|
||||||
ax6.set_ylabel('Volume (mCoins/5min)')
|
ax6.set_ylabel('Volume')
|
||||||
|
|
||||||
plt.legend(loc=3)
|
plt.legend(loc=3)
|
||||||
|
|
||||||
# Show the plot.
|
# Show the plot.
|
||||||
plt.gcf().set_size_inches(18, 8)
|
plt.gcf().set_size_inches(18, 8)
|
||||||
plt.show()
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,29 +1,49 @@
|
|||||||
'''
|
'''
|
||||||
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 run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle
|
catalyst ingest-exchange -x bitfinex -f daily -i btc_usdt
|
||||||
|
catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 \
|
||||||
|
--end 2017-9-30 -o buy_btc_simple_out.pickle
|
||||||
|
|
||||||
If you want to run this code using another exchange, make sure that
|
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 run -f buy_btc_simple.py -x poloniex --start 2016-1-1 \
|
||||||
|
--end 2017-9-30 -o buy_btc_simple_out.pickle
|
||||||
|
|
||||||
catalyst run -f buy_btc_simple.py -x poloniex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle
|
To see which assets are available on each exchange, visit:
|
||||||
|
https://www.enigma.co/catalyst/status
|
||||||
To see which assets are available on each exchange, visit:
|
|
||||||
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_usdt')
|
||||||
|
|
||||||
|
|
||||||
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='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),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
'''
|
|
||||||
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 (
|
||||||
@@ -19,58 +9,52 @@ from catalyst.api import (
|
|||||||
record,
|
record,
|
||||||
get_open_orders,
|
get_open_orders,
|
||||||
)
|
)
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats
|
from catalyst.exchange.utils.stats_utils import get_pretty_stats
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
algo_namespace = 'buy_low_sell_high_xrp'
|
algo_namespace = 'buy_the_dip_live'
|
||||||
log = Logger(algo_namespace)
|
log = Logger('buy low sell high')
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
log.info('initializing algo')
|
log.info('initializing algo')
|
||||||
context.ASSET_NAME = 'XRP_USDT'
|
context.ASSET_NAME = 'btc_usdt'
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
context.TARGET_POSITIONS = 5000
|
context.TARGET_POSITIONS = 30
|
||||||
context.PROFIT_TARGET = 0.1
|
context.PROFIT_TARGET = 0.1
|
||||||
context.SLIPPAGE_ALLOWED = 0.05
|
context.SLIPPAGE_ALLOWED = 0.02
|
||||||
|
|
||||||
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='15m'
|
frequency='1D'
|
||||||
)
|
)
|
||||||
|
|
||||||
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 = 50
|
buy_increment = 1
|
||||||
elif rsi <= 40:
|
elif rsi <= 40:
|
||||||
buy_increment = 20
|
buy_increment = 0.5
|
||||||
elif rsi <= 70:
|
elif rsi <= 70:
|
||||||
buy_increment = 5
|
buy_increment = 0.2
|
||||||
else:
|
else:
|
||||||
buy_increment = None
|
buy_increment = 0.1
|
||||||
|
|
||||||
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,
|
||||||
@@ -100,8 +84,8 @@ def _handle_data(context, data):
|
|||||||
|
|
||||||
if price < cost_basis:
|
if price < cost_basis:
|
||||||
is_buy = True
|
is_buy = True
|
||||||
elif position.amount > 0 and \
|
elif (position.amount > 0
|
||||||
price > cost_basis * (1 + context.PROFIT_TARGET):
|
and 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(
|
||||||
@@ -138,11 +122,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,
|
||||||
@@ -156,3 +140,32 @@ 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 = True
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
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'
|
|
||||||
# )
|
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import (record, symbol, order_target_percent,
|
||||||
|
get_open_orders)
|
||||||
|
from catalyst.exchange.utils.stats_utils import extract_transactions
|
||||||
|
|
||||||
|
NAMESPACE = 'dual_moving_average'
|
||||||
|
log = Logger(NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.i = 0
|
||||||
|
context.asset = symbol('ltc_usd')
|
||||||
|
context.base_price = None
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# define the windows for the moving averages
|
||||||
|
short_window = 50
|
||||||
|
long_window = 200
|
||||||
|
|
||||||
|
# Skip as many bars as long_window to properly compute the average
|
||||||
|
context.i += 1
|
||||||
|
if context.i < long_window:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Compute moving averages calling data.history() for each
|
||||||
|
# moving average with the appropriate parameters. We choose to use
|
||||||
|
# minute bars for this simulation -> freq="1m"
|
||||||
|
# Returns a pandas dataframe.
|
||||||
|
short_mavg = data.history(context.asset,
|
||||||
|
'price',
|
||||||
|
bar_count=short_window,
|
||||||
|
frequency="1m",
|
||||||
|
).mean()
|
||||||
|
long_mavg = data.history(context.asset,
|
||||||
|
'price',
|
||||||
|
bar_count=long_window,
|
||||||
|
frequency="1m",
|
||||||
|
).mean()
|
||||||
|
|
||||||
|
# Let's keep the price of our asset in a more handy variable
|
||||||
|
price = data.current(context.asset, 'price')
|
||||||
|
|
||||||
|
# If base_price is not set, we use the current value. This is the
|
||||||
|
# price at the first bar which we reference to calculate price_change.
|
||||||
|
if context.base_price is None:
|
||||||
|
context.base_price = price
|
||||||
|
price_change = (price - context.base_price) / context.base_price
|
||||||
|
|
||||||
|
# Save values for later inspection
|
||||||
|
record(price=price,
|
||||||
|
cash=context.portfolio.cash,
|
||||||
|
price_change=price_change,
|
||||||
|
short_mavg=short_mavg,
|
||||||
|
long_mavg=long_mavg)
|
||||||
|
|
||||||
|
# Since we are using limit orders, some orders may not execute immediately
|
||||||
|
# we wait until all orders are executed before considering more trades.
|
||||||
|
orders = get_open_orders(context.asset)
|
||||||
|
if len(orders) > 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Exit if we cannot trade
|
||||||
|
if not data.can_trade(context.asset):
|
||||||
|
return
|
||||||
|
|
||||||
|
# We check what's our position on our portfolio and trade accordingly
|
||||||
|
pos_amount = context.portfolio.positions[context.asset].amount
|
||||||
|
|
||||||
|
# Trading logic
|
||||||
|
if short_mavg > long_mavg and pos_amount == 0:
|
||||||
|
# we buy 100% of our portfolio for this asset
|
||||||
|
order_target_percent(context.asset, 1)
|
||||||
|
elif short_mavg < long_mavg and pos_amount > 0:
|
||||||
|
# we sell all our positions for this asset
|
||||||
|
order_target_percent(context.asset, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context, perf):
|
||||||
|
|
||||||
|
# Get the base_currency that was passed as a parameter to the simulation
|
||||||
|
exchange = list(context.exchanges.values())[0]
|
||||||
|
base_currency = exchange.base_currency.upper()
|
||||||
|
|
||||||
|
# First chart: Plot portfolio value using base_currency
|
||||||
|
ax1 = plt.subplot(411)
|
||||||
|
perf.loc[:, ['portfolio_value']].plot(ax=ax1)
|
||||||
|
ax1.legend_.remove()
|
||||||
|
ax1.set_ylabel('Portfolio Value\n({})'.format(base_currency))
|
||||||
|
start, end = ax1.get_ylim()
|
||||||
|
ax1.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||||
|
|
||||||
|
# Second chart: Plot asset price, moving averages and buys/sells
|
||||||
|
ax2 = plt.subplot(412, sharex=ax1)
|
||||||
|
perf.loc[:, ['price', 'short_mavg', 'long_mavg']].plot(
|
||||||
|
ax=ax2,
|
||||||
|
label='Price')
|
||||||
|
ax2.legend_.remove()
|
||||||
|
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||||
|
asset=context.asset.symbol,
|
||||||
|
base=base_currency
|
||||||
|
))
|
||||||
|
start, end = ax2.get_ylim()
|
||||||
|
ax2.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||||
|
|
||||||
|
transaction_df = extract_transactions(perf)
|
||||||
|
if not transaction_df.empty:
|
||||||
|
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||||
|
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||||
|
ax2.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index, 'price'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
ax2.scatter(
|
||||||
|
sell_df.index.to_pydatetime(),
|
||||||
|
perf.loc[sell_df.index, 'price'],
|
||||||
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
|
||||||
|
# Third chart: Compare percentage change between our portfolio
|
||||||
|
# and the price of the asset
|
||||||
|
ax3 = plt.subplot(413, sharex=ax1)
|
||||||
|
perf.loc[:, ['algorithm_period_return', 'price_change']].plot(ax=ax3)
|
||||||
|
ax3.legend_.remove()
|
||||||
|
ax3.set_ylabel('Percent Change')
|
||||||
|
start, end = ax3.get_ylim()
|
||||||
|
ax3.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||||
|
|
||||||
|
# Fourth chart: Plot our cash
|
||||||
|
ax4 = plt.subplot(414, sharex=ax1)
|
||||||
|
perf.cash.plot(ax=ax4)
|
||||||
|
ax4.set_ylabel('Cash\n({})'.format(base_currency))
|
||||||
|
start, end = ax4.get_ylim()
|
||||||
|
ax4.yaxis.set_ticks(np.arange(0, end, end/5))
|
||||||
|
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1000,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bitfinex',
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='usd',
|
||||||
|
start=pd.to_datetime('2017-9-22', utc=True),
|
||||||
|
end=pd.to_datetime('2017-9-23', utc=True),
|
||||||
|
)
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
# For this example, we're going to write a simple momentum script. When the
|
||||||
|
# stock goes up quickly, we're going to buy; when it goes down quickly, we're
|
||||||
|
# going to sell. Hopefully we'll ride the waves.
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol, record, order_target_percent, get_open_orders
|
||||||
|
from catalyst.exchange.utils.stats_utils import extract_transactions
|
||||||
|
# We give a name to the algorithm which Catalyst will use to persist its state.
|
||||||
|
# In this example, Catalyst will create the `.catalyst/data/live_algos`
|
||||||
|
# directory. If we stop and start the algorithm, Catalyst will resume its
|
||||||
|
# state using the files included in the folder.
|
||||||
|
from catalyst.utils.paths import ensure_directory
|
||||||
|
|
||||||
|
NAMESPACE = 'mean_reversion_simple'
|
||||||
|
log = Logger(NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
|
# To run an algorithm in Catalyst, you need two functions: initialize and
|
||||||
|
# handle_data.
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
# This initialize function sets any data or variables that you'll use in
|
||||||
|
# your algorithm. For instance, you'll want to define the trading pair (or
|
||||||
|
# trading pairs) you want to backtest. You'll also want to define any
|
||||||
|
# parameters or values you're going to use.
|
||||||
|
|
||||||
|
# In our example, we're looking at Neo in Ether.
|
||||||
|
context.market = symbol('eth_btc')
|
||||||
|
context.base_price = None
|
||||||
|
context.current_day = None
|
||||||
|
|
||||||
|
context.RSI_OVERSOLD = 55
|
||||||
|
context.RSI_OVERBOUGHT = 60
|
||||||
|
context.CANDLE_SIZE = '15T'
|
||||||
|
|
||||||
|
context.start_time = time.time()
|
||||||
|
|
||||||
|
context.set_commission(maker=0.001, taker=0.002)
|
||||||
|
context.set_slippage(spread=0.001)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# This handle_data function is where the real work is done. Our data is
|
||||||
|
# minute-level tick data, and each minute is called a frame. This function
|
||||||
|
# runs on each frame of the data.
|
||||||
|
|
||||||
|
# We flag the first period of each day.
|
||||||
|
# Since cryptocurrencies trade 24/7 the `before_trading_starts` handle
|
||||||
|
# would only execute once. This method works with minute and daily
|
||||||
|
# frequencies.
|
||||||
|
today = data.current_dt.floor('1D')
|
||||||
|
if today != context.current_day:
|
||||||
|
context.traded_today = False
|
||||||
|
context.current_day = today
|
||||||
|
|
||||||
|
# We're computing the volume-weighted-average-price of the security
|
||||||
|
# defined above, in the context.market variable. For this example, we're
|
||||||
|
# using three bars on the 15 min bars.
|
||||||
|
|
||||||
|
# The frequency attribute determine the bar size. We use this convention
|
||||||
|
# for the frequency alias:
|
||||||
|
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
|
||||||
|
prices = data.history(
|
||||||
|
context.market,
|
||||||
|
fields='close',
|
||||||
|
bar_count=50,
|
||||||
|
frequency=context.CANDLE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ta-lib calculates various technical indicator based on price and
|
||||||
|
# volume arrays.
|
||||||
|
|
||||||
|
# In this example, we are comp
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=14)
|
||||||
|
|
||||||
|
# We need a variable for the current price of the security to compare to
|
||||||
|
# the average. Since we are requesting two fields, data.current()
|
||||||
|
# returns a DataFrame with
|
||||||
|
current = data.current(context.market, fields=['close', 'volume'])
|
||||||
|
price = current['close']
|
||||||
|
|
||||||
|
# If base_price is not set, we use the current value. This is the
|
||||||
|
# price at the first bar which we reference to calculate price_change.
|
||||||
|
if context.base_price is None:
|
||||||
|
context.base_price = price
|
||||||
|
|
||||||
|
price_change = (price - context.base_price) / context.base_price
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
|
||||||
|
# Now that we've collected all current data for this frame, we use
|
||||||
|
# the record() method to save it. This data will be available as
|
||||||
|
# a parameter of the analyze() function for further analysis.
|
||||||
|
|
||||||
|
record(
|
||||||
|
volume=current['volume'],
|
||||||
|
price=price,
|
||||||
|
price_change=price_change,
|
||||||
|
rsi=rsi[-1],
|
||||||
|
cash=cash
|
||||||
|
)
|
||||||
|
# We are trying to avoid over-trading by limiting our trades to
|
||||||
|
# one per day.
|
||||||
|
if context.traded_today:
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: retest with open orders
|
||||||
|
# Since we are using limit orders, some orders may not execute immediately
|
||||||
|
# we wait until all orders are executed before considering more trades.
|
||||||
|
orders = context.blotter.open_orders
|
||||||
|
if len(orders) > 0:
|
||||||
|
log.info('exiting because orders are open: {}'.format(orders))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Exit if we cannot trade
|
||||||
|
if not data.can_trade(context.market):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Another powerful built-in feature of the Catalyst backtester is the
|
||||||
|
# portfolio object. The portfolio object tracks your positions, cash,
|
||||||
|
# cost basis of specific holdings, and more. In this line, we calculate
|
||||||
|
# how long or short our position is at this minute.
|
||||||
|
pos_amount = context.portfolio.positions[context.market].amount
|
||||||
|
|
||||||
|
if rsi[-1] <= context.RSI_OVERSOLD and pos_amount == 0:
|
||||||
|
log.info(
|
||||||
|
'{}: buying - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Set a style for limit orders,
|
||||||
|
limit_price = price * 1.005
|
||||||
|
order_target_percent(
|
||||||
|
context.market, 1, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today = True
|
||||||
|
|
||||||
|
elif rsi[-1] >= context.RSI_OVERBOUGHT and pos_amount > 0:
|
||||||
|
log.info(
|
||||||
|
'{}: selling - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
limit_price = price * 0.995
|
||||||
|
order_target_percent(
|
||||||
|
context.market, 0, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today = True
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, perf=None):
|
||||||
|
end = time.time()
|
||||||
|
log.info('elapsed time: {}'.format(end - context.start_time))
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
# The base currency of the algo exchange
|
||||||
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
|
|
||||||
|
# Plot the portfolio value over time.
|
||||||
|
ax1 = plt.subplot(611)
|
||||||
|
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||||
|
ax1.set_ylabel('Portfolio\nValue\n({})'.format(base_currency))
|
||||||
|
|
||||||
|
# Plot the price increase or decrease over time.
|
||||||
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
|
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
|
||||||
|
|
||||||
|
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||||
|
asset=context.market.symbol, base=base_currency
|
||||||
|
))
|
||||||
|
|
||||||
|
transaction_df = extract_transactions(perf)
|
||||||
|
if not transaction_df.empty:
|
||||||
|
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||||
|
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||||
|
ax2.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index.floor('1 min'), 'price'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
ax2.scatter(
|
||||||
|
sell_df.index.to_pydatetime(),
|
||||||
|
perf.loc[sell_df.index.floor('1 min'), 'price'],
|
||||||
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
|
||||||
|
ax4 = plt.subplot(613, sharex=ax1)
|
||||||
|
perf.loc[:, 'cash'].plot(
|
||||||
|
ax=ax4, label='Base Currency ({})'.format(base_currency)
|
||||||
|
)
|
||||||
|
ax4.set_ylabel('Cash\n({})'.format(base_currency))
|
||||||
|
|
||||||
|
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
|
||||||
|
|
||||||
|
ax5 = plt.subplot(614, sharex=ax1)
|
||||||
|
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
|
||||||
|
ax5.set_ylabel('Percent\nChange')
|
||||||
|
|
||||||
|
ax6 = plt.subplot(615, sharex=ax1)
|
||||||
|
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
|
||||||
|
ax6.set_ylabel('RSI')
|
||||||
|
ax6.axhline(context.RSI_OVERBOUGHT, color='darkgoldenrod')
|
||||||
|
ax6.axhline(context.RSI_OVERSOLD, color='darkgoldenrod')
|
||||||
|
|
||||||
|
if not transaction_df.empty:
|
||||||
|
ax6.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index.floor('1 min'), 'rsi'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
ax6.scatter(
|
||||||
|
sell_df.index.to_pydatetime(),
|
||||||
|
perf.loc[sell_df.index.floor('1 min'), 'rsi'],
|
||||||
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
plt.legend(loc=3)
|
||||||
|
start, end = ax6.get_ylim()
|
||||||
|
ax6.yaxis.set_ticks(np.arange(0, end, end / 5))
|
||||||
|
|
||||||
|
# Show the plot.
|
||||||
|
plt.gcf().set_size_inches(18, 8)
|
||||||
|
plt.show()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# The execution mode: backtest or live
|
||||||
|
live = True
|
||||||
|
|
||||||
|
if live:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.01,
|
||||||
|
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,
|
||||||
|
# auth_aliases=dict(poloniex='auth2')
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
folder = os.path.join(
|
||||||
|
tempfile.gettempdir(), 'catalyst', NAMESPACE
|
||||||
|
)
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
timestr = time.strftime('%Y%m%d-%H%M%S')
|
||||||
|
out = os.path.join(folder, '{}.p'.format(timestr))
|
||||||
|
# catalyst run -f catalyst/examples/mean_reversion_simple.py \
|
||||||
|
# -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion \
|
||||||
|
# --data-frequency minute --capital-base 10000
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.1,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bitfinex',
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='btc',
|
||||||
|
start=pd.to_datetime('2017-10-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-11-10', utc=True),
|
||||||
|
output=out
|
||||||
|
)
|
||||||
|
log.info('saved perf stats: {}'.format(out))
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
# For this example, we're going to write a simple momentum script. When the
|
||||||
|
# stock goes up quickly, we're going to buy; when it goes down quickly, we're
|
||||||
|
# going to sell. Hopefully we'll ride the waves.
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol, record, order_target_percent, get_open_orders
|
||||||
|
from catalyst.exchange.utils.stats_utils import extract_transactions
|
||||||
|
# We give a name to the algorithm which Catalyst will use to persist its state.
|
||||||
|
# In this example, Catalyst will create the `.catalyst/data/live_algos`
|
||||||
|
# directory. If we stop and start the algorithm, Catalyst will resume its
|
||||||
|
# state using the files included in the folder.
|
||||||
|
from catalyst.utils.paths import ensure_directory
|
||||||
|
|
||||||
|
NAMESPACE = 'mean_reversion_simple'
|
||||||
|
log = Logger(NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
|
# To run an algorithm in Catalyst, you need two functions: initialize and
|
||||||
|
# handle_data.
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
# This initialize function sets any data or variables that you'll use in
|
||||||
|
# your algorithm. For instance, you'll want to define the trading pair (or
|
||||||
|
# trading pairs) you want to backtest. You'll also want to define any
|
||||||
|
# parameters or values you're going to use.
|
||||||
|
|
||||||
|
# In our example, we're looking at Neo in Ether.
|
||||||
|
context.market = symbol('eth_btc')
|
||||||
|
context.base_price = None
|
||||||
|
context.current_day = None
|
||||||
|
|
||||||
|
context.RSI_OVERSOLD = 50
|
||||||
|
context.RSI_OVERBOUGHT = 60
|
||||||
|
context.CANDLE_SIZE = '5T'
|
||||||
|
|
||||||
|
context.start_time = time.time()
|
||||||
|
|
||||||
|
context.set_commission(maker=0.001, taker=0.002)
|
||||||
|
# context.set_slippage(spread=0.001)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# This handle_data function is where the real work is done. Our data is
|
||||||
|
# minute-level tick data, and each minute is called a frame. This function
|
||||||
|
# runs on each frame of the data.
|
||||||
|
|
||||||
|
# We flag the first period of each day.
|
||||||
|
# Since cryptocurrencies trade 24/7 the `before_trading_starts` handle
|
||||||
|
# would only execute once. This method works with minute and daily
|
||||||
|
# frequencies.
|
||||||
|
today = data.current_dt.floor('1D')
|
||||||
|
if today != context.current_day:
|
||||||
|
context.traded_today = False
|
||||||
|
context.current_day = today
|
||||||
|
|
||||||
|
# We're computing the volume-weighted-average-price of the security
|
||||||
|
# defined above, in the context.market variable. For this example, we're
|
||||||
|
# using three bars on the 15 min bars.
|
||||||
|
|
||||||
|
# The frequency attribute determine the bar size. We use this convention
|
||||||
|
# for the frequency alias:
|
||||||
|
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
|
||||||
|
prices = data.history(
|
||||||
|
context.market,
|
||||||
|
fields='close',
|
||||||
|
bar_count=50,
|
||||||
|
frequency=context.CANDLE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ta-lib calculates various technical indicator based on price and
|
||||||
|
# volume arrays.
|
||||||
|
|
||||||
|
# In this example, we are comp
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=14)
|
||||||
|
|
||||||
|
# We need a variable for the current price of the security to compare to
|
||||||
|
# the average. Since we are requesting two fields, data.current()
|
||||||
|
# returns a DataFrame with
|
||||||
|
current = data.current(context.market, fields=['close', 'volume'])
|
||||||
|
price = current['close']
|
||||||
|
|
||||||
|
# If base_price is not set, we use the current value. This is the
|
||||||
|
# price at the first bar which we reference to calculate price_change.
|
||||||
|
if context.base_price is None:
|
||||||
|
context.base_price = price
|
||||||
|
|
||||||
|
price_change = (price - context.base_price) / context.base_price
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
|
||||||
|
# Now that we've collected all current data for this frame, we use
|
||||||
|
# the record() method to save it. This data will be available as
|
||||||
|
# a parameter of the analyze() function for further analysis.
|
||||||
|
|
||||||
|
record(
|
||||||
|
volume=current['volume'],
|
||||||
|
price=price,
|
||||||
|
price_change=price_change,
|
||||||
|
rsi=rsi[-1],
|
||||||
|
cash=cash
|
||||||
|
)
|
||||||
|
# We are trying to avoid over-trading by limiting our trades to
|
||||||
|
# one per day.
|
||||||
|
if context.traded_today:
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: retest with open orders
|
||||||
|
# Since we are using limit orders, some orders may not execute immediately
|
||||||
|
# we wait until all orders are executed before considering more trades.
|
||||||
|
orders = get_open_orders(context.market)
|
||||||
|
if len(orders) > 0:
|
||||||
|
log.info('exiting because orders are open: {}'.format(orders))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Exit if we cannot trade
|
||||||
|
if not data.can_trade(context.market):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Another powerful built-in feature of the Catalyst backtester is the
|
||||||
|
# portfolio object. The portfolio object tracks your positions, cash,
|
||||||
|
# cost basis of specific holdings, and more. In this line, we calculate
|
||||||
|
# how long or short our position is at this minute.
|
||||||
|
pos_amount = context.portfolio.positions[context.market].amount
|
||||||
|
|
||||||
|
if rsi[-1] <= context.RSI_OVERSOLD and pos_amount == 0:
|
||||||
|
log.info(
|
||||||
|
'{}: buying - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Set a style for limit orders,
|
||||||
|
limit_price = price * 1.005
|
||||||
|
order_target_percent(
|
||||||
|
context.market, 1, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today = True
|
||||||
|
|
||||||
|
elif rsi[-1] >= context.RSI_OVERBOUGHT and pos_amount > 0:
|
||||||
|
log.info(
|
||||||
|
'{}: selling - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
limit_price = price * 0.995
|
||||||
|
order_target_percent(
|
||||||
|
context.market, 0, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today = True
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, perf=None):
|
||||||
|
end = time.time()
|
||||||
|
log.info('elapsed time: {}'.format(end - context.start_time))
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
# The base currency of the algo exchange
|
||||||
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
|
|
||||||
|
# Plot the portfolio value over time.
|
||||||
|
ax1 = plt.subplot(611)
|
||||||
|
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||||
|
ax1.set_ylabel('Portfolio\nValue\n({})'.format(base_currency))
|
||||||
|
|
||||||
|
# Plot the price increase or decrease over time.
|
||||||
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
|
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
|
||||||
|
|
||||||
|
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||||
|
asset=context.market.symbol, base=base_currency
|
||||||
|
))
|
||||||
|
|
||||||
|
transaction_df = extract_transactions(perf)
|
||||||
|
if not transaction_df.empty:
|
||||||
|
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||||
|
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||||
|
ax2.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index.floor('1 min'), 'price'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
ax2.scatter(
|
||||||
|
sell_df.index.to_pydatetime(),
|
||||||
|
perf.loc[sell_df.index.floor('1 min'), 'price'],
|
||||||
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
|
||||||
|
ax4 = plt.subplot(613, sharex=ax1)
|
||||||
|
perf.loc[:, 'cash'].plot(
|
||||||
|
ax=ax4, label='Base Currency ({})'.format(base_currency)
|
||||||
|
)
|
||||||
|
ax4.set_ylabel('Cash\n({})'.format(base_currency))
|
||||||
|
|
||||||
|
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
|
||||||
|
|
||||||
|
ax5 = plt.subplot(614, sharex=ax1)
|
||||||
|
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
|
||||||
|
ax5.set_ylabel('Percent\nChange')
|
||||||
|
|
||||||
|
ax6 = plt.subplot(615, sharex=ax1)
|
||||||
|
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
|
||||||
|
ax6.set_ylabel('RSI')
|
||||||
|
ax6.axhline(context.RSI_OVERBOUGHT, color='darkgoldenrod')
|
||||||
|
ax6.axhline(context.RSI_OVERSOLD, color='darkgoldenrod')
|
||||||
|
|
||||||
|
if not transaction_df.empty:
|
||||||
|
ax6.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index.floor('1 min'), 'rsi'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
ax6.scatter(
|
||||||
|
sell_df.index.to_pydatetime(),
|
||||||
|
perf.loc[sell_df.index.floor('1 min'), 'rsi'],
|
||||||
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
plt.legend(loc=3)
|
||||||
|
start, end = ax6.get_ylim()
|
||||||
|
ax6.yaxis.set_ticks(np.arange(0, end, end / 5))
|
||||||
|
|
||||||
|
# Show the plot.
|
||||||
|
plt.gcf().set_size_inches(18, 8)
|
||||||
|
plt.show()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# The execution mode: backtest or live
|
||||||
|
live = False
|
||||||
|
|
||||||
|
if live:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.025,
|
||||||
|
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(
|
||||||
|
tempfile.gettempdir(), 'catalyst', NAMESPACE
|
||||||
|
)
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
timestr = time.strftime('%Y%m%d-%H%M%S')
|
||||||
|
out = os.path.join(folder, '{}.p'.format(timestr))
|
||||||
|
# catalyst run -f catalyst/examples/mean_reversion_simple.py \
|
||||||
|
# -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion \
|
||||||
|
# --data-frequency minute --capital-base 10000
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.1,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bitfinex',
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='eth',
|
||||||
|
start=pd.to_datetime('2017-10-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-11-10', utc=True),
|
||||||
|
output=out
|
||||||
|
)
|
||||||
|
log.info('saved perf stats: {}'.format(out))
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
'''Use this code to execute a portfolio optimization model. This code
|
||||||
|
will select the portfolio with the maximum Sharpe Ratio. The parameters
|
||||||
|
are set to use 180 days of historical data and rebalance every 30 days.
|
||||||
|
|
||||||
|
This is the code used in the following article:
|
||||||
|
https://blog.enigma.co/markowitz-portfolio-optimization-for-cryptocurrencies-in-catalyst-b23c38652556
|
||||||
|
|
||||||
|
You can run this code using the Python interpreter:
|
||||||
|
|
||||||
|
$ python portfolio_optimization.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
import os
|
||||||
|
import pytz
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from catalyst.api import record, symbols, order_target_percent
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
|
np.set_printoptions(threshold='nan', suppress=True)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
# Portfolio assets list
|
||||||
|
context.assets = symbols('btc_usdt', 'eth_usdt', 'ltc_usdt', 'dash_usdt',
|
||||||
|
'xmr_usdt')
|
||||||
|
context.nassets = len(context.assets)
|
||||||
|
# Set the time window that will be used to compute expected return
|
||||||
|
# and asset correlations
|
||||||
|
context.window = 180
|
||||||
|
# Set the number of days between each portfolio rebalancing
|
||||||
|
context.rebalance_period = 30
|
||||||
|
context.i = 0
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# Only rebalance at the beggining of the algorithm execution and
|
||||||
|
# every multiple of the rebalance period
|
||||||
|
if context.i == 0 or context.i % context.rebalance_period == 0:
|
||||||
|
n = context.window
|
||||||
|
prices = data.history(context.assets, fields='price',
|
||||||
|
bar_count=n + 1, frequency='1d')
|
||||||
|
pr = np.asmatrix(prices)
|
||||||
|
t_prices = prices.iloc[1:n + 1]
|
||||||
|
t_val = t_prices.values
|
||||||
|
tminus_prices = prices.iloc[0:n]
|
||||||
|
tminus_val = tminus_prices.values
|
||||||
|
# Compute daily returns (r)
|
||||||
|
r = np.asmatrix(t_val / tminus_val - 1)
|
||||||
|
# Compute the expected returns of each asset with the average
|
||||||
|
# daily return for the selected time window
|
||||||
|
m = np.asmatrix(np.mean(r, axis=0))
|
||||||
|
# ###
|
||||||
|
stds = np.std(r, axis=0)
|
||||||
|
# Compute excess returns matrix (xr)
|
||||||
|
xr = r - m
|
||||||
|
# Matrix algebra to get variance-covariance matrix
|
||||||
|
cov_m = np.dot(np.transpose(xr), xr) / n
|
||||||
|
# Compute asset correlation matrix (informative only)
|
||||||
|
corr_m = cov_m / np.dot(np.transpose(stds), stds)
|
||||||
|
|
||||||
|
# Define portfolio optimization parameters
|
||||||
|
n_portfolios = 50000
|
||||||
|
results_array = np.zeros((3 + context.nassets, n_portfolios))
|
||||||
|
for p in xrange(n_portfolios):
|
||||||
|
weights = np.random.random(context.nassets)
|
||||||
|
weights /= np.sum(weights)
|
||||||
|
w = np.asmatrix(weights)
|
||||||
|
p_r = np.sum(np.dot(w, np.transpose(m))) * 365
|
||||||
|
p_std = np.sqrt(np.dot(np.dot(w, cov_m),
|
||||||
|
np.transpose(w))) * np.sqrt(365)
|
||||||
|
|
||||||
|
# store results in results array
|
||||||
|
results_array[0, p] = p_r
|
||||||
|
results_array[1, p] = p_std
|
||||||
|
# store Sharpe Ratio (return / volatility) - risk free rate element
|
||||||
|
# excluded for simplicity
|
||||||
|
results_array[2, p] = results_array[0, p] / results_array[1, p]
|
||||||
|
i = 0
|
||||||
|
for iw in weights:
|
||||||
|
results_array[3 + i, p] = weights[i]
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# convert results array to Pandas DataFrame
|
||||||
|
results_frame = pd.DataFrame(np.transpose(results_array),
|
||||||
|
columns=['r', 'stdev', 'sharpe']
|
||||||
|
+ context.assets)
|
||||||
|
# locate position of portfolio with highest Sharpe Ratio
|
||||||
|
max_sharpe_port = results_frame.iloc[results_frame['sharpe'].idxmax()]
|
||||||
|
# locate positon of portfolio with minimum standard deviation
|
||||||
|
# min_vol_port = results_frame.iloc[results_frame['stdev'].idxmin()]
|
||||||
|
|
||||||
|
# order optimal weights for each asset
|
||||||
|
for asset in context.assets:
|
||||||
|
if data.can_trade(asset):
|
||||||
|
order_target_percent(asset, max_sharpe_port[asset])
|
||||||
|
|
||||||
|
# create scatter plot coloured by Sharpe Ratio
|
||||||
|
plt.scatter(results_frame.stdev,
|
||||||
|
results_frame.r,
|
||||||
|
c=results_frame.sharpe,
|
||||||
|
cmap='RdYlGn')
|
||||||
|
plt.xlabel('Volatility')
|
||||||
|
plt.ylabel('Returns')
|
||||||
|
plt.colorbar()
|
||||||
|
# plot red star to highlight position of portfolio
|
||||||
|
# with highest Sharpe Ratio
|
||||||
|
plt.scatter(max_sharpe_port[1],
|
||||||
|
max_sharpe_port[0],
|
||||||
|
marker='o',
|
||||||
|
color='b',
|
||||||
|
s=200)
|
||||||
|
# plot green star to highlight position of minimum variance portfolio
|
||||||
|
plt.show()
|
||||||
|
print(max_sharpe_port)
|
||||||
|
record(pr=pr,
|
||||||
|
r=r,
|
||||||
|
m=m,
|
||||||
|
stds=stds,
|
||||||
|
max_sharpe_port=max_sharpe_port,
|
||||||
|
corr_m=corr_m)
|
||||||
|
context.i += 1
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, results=None):
|
||||||
|
# Form DataFrame with selected data
|
||||||
|
data = results[['pr', 'r', 'm', 'stds', 'max_sharpe_port', 'corr_m',
|
||||||
|
'portfolio_value']]
|
||||||
|
|
||||||
|
# Save results in CSV file
|
||||||
|
filename = os.path.splitext(os.path.basename(__file__))[0]
|
||||||
|
data.to_csv(filename + '.csv')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 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,265 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst.api import (
|
||||||
|
order,
|
||||||
|
symbol,
|
||||||
|
record,
|
||||||
|
get_open_orders,
|
||||||
|
)
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
|
algo_namespace = 'rsi'
|
||||||
|
log = Logger(algo_namespace)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
log.info('initializing algo')
|
||||||
|
context.asset = symbol('eth_btc')
|
||||||
|
context.base_price = None
|
||||||
|
|
||||||
|
context.MAX_HOLDINGS = 0.2
|
||||||
|
context.RSI_OVERSOLD = 30
|
||||||
|
context.RSI_OVERSOLD_BBANDS = 45
|
||||||
|
context.RSI_OVERBOUGHT_BBANDS = 55
|
||||||
|
context.SLIPPAGE_ALLOWED = 0.03
|
||||||
|
|
||||||
|
context.TARGET = 0.15
|
||||||
|
context.STOP_LOSS = 0.1
|
||||||
|
context.STOP = 0.03
|
||||||
|
context.position = None
|
||||||
|
|
||||||
|
context.last_bar = None
|
||||||
|
|
||||||
|
context.errors = []
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_buy_sell_decision(context, data, signal, price):
|
||||||
|
orders = get_open_orders(context.asset)
|
||||||
|
if len(orders) > 0:
|
||||||
|
log.info('skipping bar until all open orders execute')
|
||||||
|
return
|
||||||
|
|
||||||
|
positions = context.portfolio.positions
|
||||||
|
if context.position is None and context.asset in positions:
|
||||||
|
position = positions[context.asset]
|
||||||
|
context.position = dict(
|
||||||
|
cost_basis=position['cost_basis'],
|
||||||
|
amount=position['amount'],
|
||||||
|
stop=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# action = None
|
||||||
|
if context.position is not None:
|
||||||
|
cost_basis = context.position['cost_basis']
|
||||||
|
amount = context.position['amount']
|
||||||
|
log.info(
|
||||||
|
'found {amount} positions with cost basis {cost_basis}'.format(
|
||||||
|
amount=amount,
|
||||||
|
cost_basis=cost_basis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stop = context.position['stop']
|
||||||
|
|
||||||
|
target = cost_basis * (1 + context.TARGET)
|
||||||
|
if price >= target:
|
||||||
|
context.position['cost_basis'] = price
|
||||||
|
context.position['stop'] = context.STOP
|
||||||
|
|
||||||
|
stop_target = context.STOP_LOSS if stop is None else context.STOP
|
||||||
|
if price < cost_basis * (1 - stop_target):
|
||||||
|
log.info('executing stop loss')
|
||||||
|
order(
|
||||||
|
asset=context.asset,
|
||||||
|
amount=-amount,
|
||||||
|
limit_price=price * (1 - context.SLIPPAGE_ALLOWED),
|
||||||
|
)
|
||||||
|
# action = 0
|
||||||
|
context.position = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
if signal == 'long':
|
||||||
|
log.info('opening position')
|
||||||
|
buy_amount = context.MAX_HOLDINGS / price
|
||||||
|
order(
|
||||||
|
asset=context.asset,
|
||||||
|
amount=buy_amount,
|
||||||
|
limit_price=price * (1 + context.SLIPPAGE_ALLOWED),
|
||||||
|
)
|
||||||
|
context.position = dict(
|
||||||
|
cost_basis=price,
|
||||||
|
amount=buy_amount,
|
||||||
|
stop=None
|
||||||
|
)
|
||||||
|
# action = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_data_rsi_only(context, data):
|
||||||
|
price = data.current(context.asset, 'close')
|
||||||
|
log.info('got price {price}'.format(price=price))
|
||||||
|
|
||||||
|
if price is np.nan:
|
||||||
|
log.warn('no pricing data')
|
||||||
|
return
|
||||||
|
|
||||||
|
if context.base_price is None:
|
||||||
|
context.base_price = price
|
||||||
|
|
||||||
|
try:
|
||||||
|
prices = data.history(
|
||||||
|
context.asset,
|
||||||
|
fields='price',
|
||||||
|
bar_count=20,
|
||||||
|
frequency='30T'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warn('historical data not available: '.format(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=16)[-1]
|
||||||
|
log.info('got rsi {}'.format(rsi))
|
||||||
|
|
||||||
|
signal = None
|
||||||
|
if rsi < context.RSI_OVERSOLD:
|
||||||
|
signal = 'long'
|
||||||
|
|
||||||
|
# Making sure that the price is still current
|
||||||
|
price = data.current(context.asset, 'close')
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
log.info(
|
||||||
|
'base currency available: {cash}, cap: {cap}'.format(
|
||||||
|
cash=cash,
|
||||||
|
cap=context.MAX_HOLDINGS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
volume = data.current(context.asset, 'volume')
|
||||||
|
price_change = (price - context.base_price) / context.base_price
|
||||||
|
record(
|
||||||
|
price=price,
|
||||||
|
price_change=price_change,
|
||||||
|
rsi=rsi,
|
||||||
|
volume=volume,
|
||||||
|
cash=cash,
|
||||||
|
starting_cash=context.portfolio.starting_cash,
|
||||||
|
leverage=context.account.leverage,
|
||||||
|
)
|
||||||
|
|
||||||
|
_handle_buy_sell_decision(context, data, signal, price)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
dt = data.current_dt
|
||||||
|
|
||||||
|
if context.last_bar is None or (
|
||||||
|
context.last_bar + timedelta(minutes=15)) <= dt:
|
||||||
|
context.last_bar = dt
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
log.info('BAR {}'.format(dt))
|
||||||
|
try:
|
||||||
|
_handle_data_rsi_only(context, data)
|
||||||
|
except Exception as e:
|
||||||
|
log.warn('aborting the bar on error {}'.format(e))
|
||||||
|
context.errors.append(e)
|
||||||
|
|
||||||
|
if len(context.errors) > 0:
|
||||||
|
log.info('the errors:\n{}'.format(context.errors))
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, results=None):
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
|
# Plot the portfolio and asset data.
|
||||||
|
ax1 = plt.subplot(611)
|
||||||
|
results.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||||
|
ax1.set_ylabel('Portfolio Value ({})'.format(base_currency))
|
||||||
|
|
||||||
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
|
results.loc[:, 'price'].plot(ax=ax2)
|
||||||
|
ax2.set_ylabel('{asset} ({base})'.format(
|
||||||
|
asset=context.asset.symbol, base=base_currency
|
||||||
|
))
|
||||||
|
|
||||||
|
trans = results.loc[[t != [] for t in results.transactions], :]
|
||||||
|
buys = trans.loc[[t[0]['amount'] > 0 for t in trans.transactions], :]
|
||||||
|
sells = trans.loc[[t[0]['amount'] < 0 for t in trans.transactions], :]
|
||||||
|
# buys = results.loc[results['action'] == 1, :]
|
||||||
|
# sells = results.loc[results['action'] == 0, :]
|
||||||
|
|
||||||
|
ax2.plot(
|
||||||
|
buys.index,
|
||||||
|
results.loc[buys.index, 'price'],
|
||||||
|
'^',
|
||||||
|
markersize=10,
|
||||||
|
color='g',
|
||||||
|
)
|
||||||
|
ax2.plot(
|
||||||
|
sells.index,
|
||||||
|
results.loc[sells.index, 'price'],
|
||||||
|
'v',
|
||||||
|
markersize=10,
|
||||||
|
color='r',
|
||||||
|
)
|
||||||
|
|
||||||
|
ax3 = plt.subplot(613, sharex=ax1)
|
||||||
|
results.loc[:, ['alpha', 'beta']].plot(ax=ax3)
|
||||||
|
ax3.set_ylabel('Alpha / Beta ')
|
||||||
|
|
||||||
|
ax4 = plt.subplot(614, sharex=ax1)
|
||||||
|
results.loc[:, ['starting_cash', 'cash']].plot(ax=ax4)
|
||||||
|
ax4.set_ylabel('Base Currency ({})'.format(base_currency))
|
||||||
|
|
||||||
|
results['algorithm'] = results.loc[:, 'algorithm_period_return']
|
||||||
|
|
||||||
|
ax5 = plt.subplot(615, sharex=ax1)
|
||||||
|
results.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
|
||||||
|
ax5.set_ylabel('Percent Change')
|
||||||
|
|
||||||
|
ax6 = plt.subplot(616, sharex=ax1)
|
||||||
|
results.loc[:, 'rsi'].plot(ax=ax6)
|
||||||
|
ax6.set_ylabel('RSI')
|
||||||
|
|
||||||
|
ax6.plot(
|
||||||
|
buys.index,
|
||||||
|
results.loc[buys.index, 'rsi'],
|
||||||
|
'^',
|
||||||
|
markersize=10,
|
||||||
|
color='g',
|
||||||
|
)
|
||||||
|
ax6.plot(
|
||||||
|
sells.index,
|
||||||
|
results.loc[sells.index, 'rsi'],
|
||||||
|
'v',
|
||||||
|
markersize=10,
|
||||||
|
color='r',
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.legend(loc=3)
|
||||||
|
|
||||||
|
# Show the plot.
|
||||||
|
plt.gcf().set_size_inches(18, 8)
|
||||||
|
plt.show()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Backtest
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.5,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace=algo_namespace,
|
||||||
|
base_currency='btc',
|
||||||
|
start=pd.to_datetime('2017-9-1', utc=True),
|
||||||
|
end=pd.to_datetime('2017-10-1', utc=True),
|
||||||
|
)
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,52 +1,144 @@
|
|||||||
import talib
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import talib
|
||||||
|
from logbook import Logger, INFO
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
from catalyst import run_algorithm
|
||||||
from catalyst.api import symbol
|
from catalyst.api import symbol, record
|
||||||
|
from catalyst.exchange.utils.stats_utils import get_pretty_stats, \
|
||||||
|
extract_transactions
|
||||||
|
|
||||||
|
log = Logger('simple_loop', level=INFO)
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
print('initializing')
|
log.info('initializing')
|
||||||
context.asset = symbol('eth_btc')
|
context.asset = symbol('eth_btc')
|
||||||
|
context.base_price = None
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
print('handling bar: {}'.format(data.current_dt))
|
log.info('handling bar: {}'.format(data.current_dt))
|
||||||
|
|
||||||
price = data.current(context.asset, 'close')
|
price = data.current(context.asset, 'close')
|
||||||
print('got price {price}'.format(price=price))
|
log.info('got price {price}'.format(price=price))
|
||||||
|
|
||||||
try:
|
prices = data.history(
|
||||||
prices = data.history(
|
context.asset,
|
||||||
context.asset,
|
fields='price',
|
||||||
fields='price',
|
bar_count=20,
|
||||||
bar_count=16,
|
frequency='30T'
|
||||||
frequency='5T'
|
)
|
||||||
|
last_traded = prices.index[-1]
|
||||||
|
log.info('last candle date: {}'.format(last_traded))
|
||||||
|
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
||||||
|
log.info('got rsi: {}'.format(rsi))
|
||||||
|
|
||||||
|
# If base_price is not set, we use the current value. This is the
|
||||||
|
# price at the first bar which we reference to calculate price_change.
|
||||||
|
if context.base_price is None:
|
||||||
|
context.base_price = price
|
||||||
|
|
||||||
|
price_change = (price - context.base_price) / context.base_price
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
|
||||||
|
# Now that we've collected all current data for this frame, we use
|
||||||
|
# the record() method to save it. This data will be available as
|
||||||
|
# a parameter of the analyze() function for further analysis.
|
||||||
|
record(
|
||||||
|
price=price,
|
||||||
|
price_change=price_change,
|
||||||
|
cash=cash
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context, perf):
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
log.info('the stats: {}'.format(get_pretty_stats(perf)))
|
||||||
|
|
||||||
|
# The base currency of the algo exchange
|
||||||
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
|
|
||||||
|
# Plot the portfolio value over time.
|
||||||
|
ax1 = plt.subplot(611)
|
||||||
|
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||||
|
ax1.set_ylabel('Portfolio Value ({})'.format(base_currency))
|
||||||
|
|
||||||
|
# Plot the price increase or decrease over time.
|
||||||
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
|
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
|
||||||
|
|
||||||
|
ax2.set_ylabel('{asset} ({base})'.format(
|
||||||
|
asset=context.asset.symbol, base=base_currency
|
||||||
|
))
|
||||||
|
|
||||||
|
transaction_df = extract_transactions(perf)
|
||||||
|
if not transaction_df.empty:
|
||||||
|
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||||
|
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||||
|
ax2.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index, 'price'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
)
|
)
|
||||||
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
ax2.scatter(
|
||||||
print('got rsi: {}'.format(rsi))
|
sell_df.index.to_pydatetime(),
|
||||||
except Exception as e:
|
perf.loc[sell_df.index, 'price'],
|
||||||
print(e)
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
|
||||||
|
ax4 = plt.subplot(613, sharex=ax1)
|
||||||
|
perf.loc[:, 'cash'].plot(
|
||||||
|
ax=ax4, label='Base Currency ({})'.format(base_currency)
|
||||||
|
)
|
||||||
|
ax4.set_ylabel('Cash ({})'.format(base_currency))
|
||||||
|
|
||||||
|
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
|
||||||
|
|
||||||
|
ax5 = plt.subplot(614, sharex=ax1)
|
||||||
|
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
|
||||||
|
ax5.set_ylabel('Percent Change')
|
||||||
|
|
||||||
|
plt.legend(loc=3)
|
||||||
|
|
||||||
|
# Show the plot.
|
||||||
|
plt.gcf().set_size_inches(18, 8)
|
||||||
|
plt.show()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
run_algorithm(
|
if __name__ == '__main__':
|
||||||
capital_base=250,
|
mode = 'backtest'
|
||||||
start=pd.to_datetime('2016-6-1', utc=True),
|
|
||||||
end=pd.to_datetime('2016-12-31', utc=True),
|
if mode == 'backtest':
|
||||||
data_frequency='daily',
|
run_algorithm(
|
||||||
initialize=initialize,
|
capital_base=1,
|
||||||
handle_data=handle_data,
|
initialize=initialize,
|
||||||
analyze=None,
|
handle_data=handle_data,
|
||||||
exchange_name='bitfinex',
|
analyze=None,
|
||||||
algo_namespace='simple_loop',
|
exchange_name='poloniex',
|
||||||
base_currency='btc'
|
algo_namespace='simple_loop',
|
||||||
)
|
base_currency='eth',
|
||||||
# run_algorithm(
|
data_frequency='minute',
|
||||||
# initialize=initialize,
|
start=pd.to_datetime('2017-9-1', utc=True),
|
||||||
# handle_data=handle_data,
|
end=pd.to_datetime('2017-12-1', utc=True),
|
||||||
# analyze=None,
|
)
|
||||||
# exchange_name='poloniex',
|
else:
|
||||||
# live=True,
|
run_algorithm(
|
||||||
# algo_namespace='simple_loop',
|
capital_base=1,
|
||||||
# base_currency='eth',
|
initialize=initialize,
|
||||||
# live_graph=False
|
handle_data=handle_data,
|
||||||
|
analyze=None,
|
||||||
|
exchange_name='binance',
|
||||||
|
live=True,
|
||||||
|
algo_namespace='simple_loop',
|
||||||
|
base_currency='eth',
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=True
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
"""
|
||||||
|
Requires Catalyst version 0.3.0 or above
|
||||||
|
Tested on Catalyst version 0.3.3
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
catalyst ingest-exchange -x bitfinex -f minute
|
||||||
|
|
||||||
|
python simple_universe.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import (symbols, )
|
||||||
|
from catalyst.exchange.utils.exchange_utils import get_exchange_symbols
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.i = -1 # minute counter
|
||||||
|
context.exchange = list(context.exchanges.values())[0].name.lower()
|
||||||
|
context.base_currency = list(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 = int(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 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='poloniex',
|
||||||
|
data_frequency='minute',
|
||||||
|
base_currency='btc',
|
||||||
|
live=False,
|
||||||
|
live_graph=False,
|
||||||
|
algo_namespace='simple_universe')
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
# Run Command
|
||||||
|
# catalyst run --start 2017-1-1 --end 2017-11-1 -o talib_simple.pickle \
|
||||||
|
# -f talib_simple.py -x poloniex
|
||||||
|
#
|
||||||
|
# Description
|
||||||
|
# Simple TALib Example showing how to use various indicators
|
||||||
|
# in you strategy. Based loosly on
|
||||||
|
# https://github.com/mellertson/talib-macd-example/blob/master/talib-macd-matplotlib-example.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib as ta
|
||||||
|
from logbook import Logger
|
||||||
|
from matplotlib.dates import date2num
|
||||||
|
from matplotlib.finance import candlestick_ohlc
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import (
|
||||||
|
order,
|
||||||
|
order_target_percent,
|
||||||
|
symbol,
|
||||||
|
)
|
||||||
|
from catalyst.exchange.utils.stats_utils import get_pretty_stats
|
||||||
|
|
||||||
|
algo_namespace = 'talib_sample'
|
||||||
|
log = Logger(algo_namespace)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
log.info('Starting TALib Simple Example')
|
||||||
|
|
||||||
|
context.ASSET_NAME = 'BTC_USDT'
|
||||||
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
|
context.ORDER_SIZE = 10
|
||||||
|
context.SLIPPAGE_ALLOWED = 0.05
|
||||||
|
|
||||||
|
context.swallow_errors = True
|
||||||
|
context.errors = []
|
||||||
|
|
||||||
|
# Bars to look at per iteration should be bigger than SMA_SLOW
|
||||||
|
context.BARS = 365
|
||||||
|
context.COUNT = 0
|
||||||
|
|
||||||
|
# Technical Analysis Settings
|
||||||
|
context.SMA_FAST = 50
|
||||||
|
context.SMA_SLOW = 100
|
||||||
|
context.RSI_PERIOD = 14
|
||||||
|
context.RSI_OVER_BOUGHT = 80
|
||||||
|
context.RSI_OVER_SOLD = 20
|
||||||
|
context.RSI_AVG_PERIOD = 15
|
||||||
|
context.MACD_FAST = 12
|
||||||
|
context.MACD_SLOW = 26
|
||||||
|
context.MACD_SIGNAL = 9
|
||||||
|
context.STOCH_K = 14
|
||||||
|
context.STOCH_D = 3
|
||||||
|
context.STOCH_OVER_BOUGHT = 80
|
||||||
|
context.STOCH_OVER_SOLD = 20
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_data(context, data):
|
||||||
|
# Get price, open, high, low, close
|
||||||
|
prices = data.history(
|
||||||
|
context.asset,
|
||||||
|
bar_count=context.BARS,
|
||||||
|
fields=['price', 'open', 'high', 'low', 'close'],
|
||||||
|
frequency='1d')
|
||||||
|
|
||||||
|
# Create a analysis data frame
|
||||||
|
analysis = pd.DataFrame(index=prices.index)
|
||||||
|
|
||||||
|
# SMA FAST
|
||||||
|
analysis['sma_f'] = ta.SMA(prices.close.as_matrix(), context.SMA_FAST)
|
||||||
|
# SMA SLOW
|
||||||
|
analysis['sma_s'] = ta.SMA(prices.close.as_matrix(), context.SMA_SLOW)
|
||||||
|
|
||||||
|
# Relative Strength Index
|
||||||
|
analysis['rsi'] = ta.RSI(prices.close.as_matrix(), context.RSI_PERIOD)
|
||||||
|
# RSI SMA
|
||||||
|
analysis['sma_r'] = ta.SMA(analysis.rsi.as_matrix(),
|
||||||
|
context.RSI_AVG_PERIOD)
|
||||||
|
|
||||||
|
# MACD, MACD Signal, MACD Histogram
|
||||||
|
analysis['macd'], analysis['macdSignal'], analysis['macdHist'] = ta.MACD(
|
||||||
|
prices.close.as_matrix(), fastperiod=context.MACD_FAST,
|
||||||
|
slowperiod=context.MACD_SLOW, signalperiod=context.MACD_SIGNAL)
|
||||||
|
|
||||||
|
# Stochastics %K %D
|
||||||
|
# %K = (Current Close - Lowest Low)/(Highest High - Lowest Low) * 100
|
||||||
|
# %D = 3-day SMA of %K
|
||||||
|
analysis['stoch_k'], analysis['stoch_d'] = ta.STOCH(
|
||||||
|
prices.high.as_matrix(), prices.low.as_matrix(),
|
||||||
|
prices.close.as_matrix(), slowk_period=context.STOCH_K,
|
||||||
|
slowd_period=context.STOCH_D)
|
||||||
|
|
||||||
|
# SMA FAST over SLOW Crossover
|
||||||
|
analysis['sma_test'] = np.where(analysis.sma_f > analysis.sma_s, 1, 0)
|
||||||
|
|
||||||
|
# MACD over Signal Crossover
|
||||||
|
analysis['macd_test'] = np.where((analysis.macd > analysis.macdSignal), 1,
|
||||||
|
0)
|
||||||
|
|
||||||
|
# Stochastics OVER BOUGHT & Decreasing
|
||||||
|
analysis['stoch_over_bought'] = np.where(
|
||||||
|
(analysis.stoch_k > context.STOCH_OVER_BOUGHT) & (
|
||||||
|
analysis.stoch_k > analysis.stoch_k.shift(1)), 1, 0)
|
||||||
|
|
||||||
|
# Stochastics OVER SOLD & Increasing
|
||||||
|
analysis['stoch_over_sold'] = np.where(
|
||||||
|
(analysis.stoch_k < context.STOCH_OVER_SOLD) & (
|
||||||
|
analysis.stoch_k > analysis.stoch_k.shift(1)), 1, 0)
|
||||||
|
|
||||||
|
# RSI OVER BOUGHT & Decreasing
|
||||||
|
analysis['rsi_over_bought'] = np.where(
|
||||||
|
(analysis.rsi > context.RSI_OVER_BOUGHT) & (
|
||||||
|
analysis.rsi < analysis.rsi.shift(1)), 1, 0)
|
||||||
|
|
||||||
|
# RSI OVER SOLD & Increasing
|
||||||
|
analysis['rsi_over_sold'] = np.where(
|
||||||
|
(analysis.rsi < context.RSI_OVER_SOLD) & (
|
||||||
|
analysis.rsi > analysis.rsi.shift(1)), 1, 0)
|
||||||
|
|
||||||
|
# Save the prices and analysis to send to analyze
|
||||||
|
context.prices = prices
|
||||||
|
context.analysis = analysis
|
||||||
|
context.price = data.current(context.asset, 'price')
|
||||||
|
|
||||||
|
makeOrders(context, analysis)
|
||||||
|
|
||||||
|
# Log the values of this bar
|
||||||
|
logAnalysis(analysis)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
log.info('handling bar {}'.format(data.current_dt))
|
||||||
|
try:
|
||||||
|
_handle_data(context, data)
|
||||||
|
except Exception as e:
|
||||||
|
log.warn('aborting the bar on error {}'.format(e))
|
||||||
|
context.errors.append(e)
|
||||||
|
|
||||||
|
log.info('completed bar {}, total execution errors {}'.format(
|
||||||
|
data.current_dt,
|
||||||
|
len(context.errors)
|
||||||
|
))
|
||||||
|
|
||||||
|
if len(context.errors) > 0:
|
||||||
|
log.info('the errors:\n{}'.format(context.errors))
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context, results):
|
||||||
|
# Save results in CSV file
|
||||||
|
filename = os.path.splitext(os.path.basename('talib_simple'))[0]
|
||||||
|
results.to_csv(filename + '.csv')
|
||||||
|
|
||||||
|
log.info('the daily stats:\n{}'.format(get_pretty_stats(results)))
|
||||||
|
chart(context, context.prices, context.analysis, results)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def makeOrders(context, analysis):
|
||||||
|
if context.asset in context.portfolio.positions:
|
||||||
|
|
||||||
|
# Current position
|
||||||
|
position = context.portfolio.positions[context.asset]
|
||||||
|
|
||||||
|
if (position == 0):
|
||||||
|
log.info('Position Zero')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Cost Basis
|
||||||
|
cost_basis = position.cost_basis
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'Holdings: {amount} @ {cost_basis}'.format(
|
||||||
|
amount=position.amount,
|
||||||
|
cost_basis=cost_basis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sell when holding and got sell singnal
|
||||||
|
if isSell(context, analysis):
|
||||||
|
profit = (context.price * position.amount) - (
|
||||||
|
cost_basis * position.amount)
|
||||||
|
order_target_percent(
|
||||||
|
asset=context.asset,
|
||||||
|
target=0,
|
||||||
|
limit_price=context.price * (1 - context.SLIPPAGE_ALLOWED),
|
||||||
|
)
|
||||||
|
log.info(
|
||||||
|
'Sold {amount} @ {price} Profit: {profit}'.format(
|
||||||
|
amount=position.amount,
|
||||||
|
price=context.price,
|
||||||
|
profit=profit
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.info('no buy or sell opportunity found')
|
||||||
|
else:
|
||||||
|
# Buy when not holding and got buy signal
|
||||||
|
if isBuy(context, analysis):
|
||||||
|
order(
|
||||||
|
asset=context.asset,
|
||||||
|
amount=context.ORDER_SIZE,
|
||||||
|
limit_price=context.price * (1 + context.SLIPPAGE_ALLOWED)
|
||||||
|
)
|
||||||
|
log.info(
|
||||||
|
'Bought {amount} @ {price}'.format(
|
||||||
|
amount=context.ORDER_SIZE,
|
||||||
|
price=context.price
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def isBuy(context, analysis):
|
||||||
|
# Bullish SMA Crossover
|
||||||
|
if (getLast(analysis, 'sma_test') == 1):
|
||||||
|
# Bullish MACD
|
||||||
|
if (getLast(analysis, 'macd_test') == 1):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# # Bullish Stochastics
|
||||||
|
# if(getLast(analysis, 'stoch_over_sold') == 1):
|
||||||
|
# return True
|
||||||
|
|
||||||
|
# # Bullish RSI
|
||||||
|
# if(getLast(analysis, 'rsi_over_sold') == 1):
|
||||||
|
# return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def isSell(context, analysis):
|
||||||
|
# Bearish SMA Crossover
|
||||||
|
if (getLast(analysis, 'sma_test') == 0):
|
||||||
|
# Bearish MACD
|
||||||
|
if (getLast(analysis, 'macd_test') == 0):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# # Bearish Stochastics
|
||||||
|
# if(getLast(analysis, 'stoch_over_bought') == 0):
|
||||||
|
# return True
|
||||||
|
|
||||||
|
# # Bearish RSI
|
||||||
|
# if(getLast(analysis, 'rsi_over_bought') == 0):
|
||||||
|
# return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def chart(context, prices, analysis, results):
|
||||||
|
results.portfolio_value.plot()
|
||||||
|
|
||||||
|
# Data for matplotlib finance plot
|
||||||
|
dates = date2num(prices.index.to_pydatetime())
|
||||||
|
|
||||||
|
# Create the Open High Low Close Tuple
|
||||||
|
prices_ohlc = [tuple([dates[i],
|
||||||
|
prices.open[i],
|
||||||
|
prices.high[i],
|
||||||
|
prices.low[i],
|
||||||
|
prices.close[i]]) for i in range(len(dates))]
|
||||||
|
|
||||||
|
fig = plt.figure(figsize=(14, 18))
|
||||||
|
|
||||||
|
# Draw the candle sticks
|
||||||
|
ax1 = fig.add_subplot(411)
|
||||||
|
ax1.set_ylabel(context.ASSET_NAME, size=20)
|
||||||
|
candlestick_ohlc(ax1, prices_ohlc, width=0.4, colorup='g', colordown='r')
|
||||||
|
|
||||||
|
# Draw Moving Averages
|
||||||
|
analysis.sma_f.plot(ax=ax1, c='r')
|
||||||
|
analysis.sma_s.plot(ax=ax1, c='g')
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
ax2 = fig.add_subplot(412)
|
||||||
|
ax2.set_ylabel('RSI', size=12)
|
||||||
|
analysis.rsi.plot(ax=ax2, c='g',
|
||||||
|
label='Period: ' + str(context.RSI_PERIOD))
|
||||||
|
analysis.sma_r.plot(ax=ax2, c='r',
|
||||||
|
label='MA: ' + str(context.RSI_AVG_PERIOD))
|
||||||
|
ax2.axhline(y=30, c='b')
|
||||||
|
ax2.axhline(y=50, c='black')
|
||||||
|
ax2.axhline(y=70, c='b')
|
||||||
|
ax2.set_ylim([0, 100])
|
||||||
|
handles, labels = ax2.get_legend_handles_labels()
|
||||||
|
ax2.legend(handles, labels)
|
||||||
|
|
||||||
|
# Draw MACD computed with Talib
|
||||||
|
ax3 = fig.add_subplot(413)
|
||||||
|
ax3.set_ylabel('MACD: ' + str(context.MACD_FAST) + ', ' + str(
|
||||||
|
context.MACD_SLOW) + ', ' + str(context.MACD_SIGNAL), size=12)
|
||||||
|
analysis.macd.plot(ax=ax3, color='b', label='Macd')
|
||||||
|
analysis.macdSignal.plot(ax=ax3, color='g', label='Signal')
|
||||||
|
analysis.macdHist.plot(ax=ax3, color='r', label='Hist')
|
||||||
|
ax3.axhline(0, lw=2, color='0')
|
||||||
|
handles, labels = ax3.get_legend_handles_labels()
|
||||||
|
ax3.legend(handles, labels)
|
||||||
|
|
||||||
|
# Stochastic plot
|
||||||
|
ax4 = fig.add_subplot(414)
|
||||||
|
ax4.set_ylabel('Stoch (k,d)', size=12)
|
||||||
|
analysis.stoch_k.plot(ax=ax4, label='stoch_k:' + str(context.STOCH_K),
|
||||||
|
color='r')
|
||||||
|
analysis.stoch_d.plot(ax=ax4, label='stoch_d:' + str(context.STOCH_D),
|
||||||
|
color='g')
|
||||||
|
handles, labels = ax4.get_legend_handles_labels()
|
||||||
|
ax4.legend(handles, labels)
|
||||||
|
ax4.axhline(y=20, c='b')
|
||||||
|
ax4.axhline(y=50, c='black')
|
||||||
|
ax4.axhline(y=80, c='b')
|
||||||
|
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def logAnalysis(analysis):
|
||||||
|
# Log only the last value in the array
|
||||||
|
log.info('- sma_f: {:.2f}'.format(getLast(analysis, 'sma_f')))
|
||||||
|
log.info('- sma_s: {:.2f}'.format(getLast(analysis, 'sma_s')))
|
||||||
|
|
||||||
|
log.info('- rsi: {:.2f}'.format(getLast(analysis, 'rsi')))
|
||||||
|
log.info('- sma_r: {:.2f}'.format(getLast(analysis, 'sma_r')))
|
||||||
|
|
||||||
|
log.info('- macd: {:.2f}'.format(getLast(analysis, 'macd')))
|
||||||
|
log.info(
|
||||||
|
'- macdSignal: {:.2f}'.format(getLast(analysis, 'macdSignal')))
|
||||||
|
log.info('- macdHist: {:.2f}'.format(getLast(analysis, 'macdHist')))
|
||||||
|
|
||||||
|
log.info('- stoch_k: {:.2f}'.format(getLast(analysis, 'stoch_k')))
|
||||||
|
log.info('- stoch_d: {:.2f}'.format(getLast(analysis, 'stoch_d')))
|
||||||
|
|
||||||
|
log.info('- sma_test: {}'.format(getLast(analysis, 'sma_test')))
|
||||||
|
log.info('- macd_test: {}'.format(getLast(analysis, 'macd_test')))
|
||||||
|
|
||||||
|
log.info('- stoch_over_bought: {}'.format(
|
||||||
|
getLast(analysis, 'stoch_over_bought')))
|
||||||
|
log.info(
|
||||||
|
'- stoch_over_sold: {}'.format(getLast(analysis, 'stoch_over_sold')))
|
||||||
|
|
||||||
|
log.info('- rsi_over_bought: {}'.format(
|
||||||
|
getLast(analysis, 'rsi_over_bought')))
|
||||||
|
log.info(
|
||||||
|
'- rsi_over_sold: {}'.format(getLast(analysis, 'rsi_over_sold')))
|
||||||
|
|
||||||
|
|
||||||
|
def getLast(arr, name):
|
||||||
|
return arr[name][arr[name].index[-1]]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=10000,
|
||||||
|
data_frequency='daily',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
base_currency='usdt',
|
||||||
|
start=pd.to_datetime('2016-11-1', utc=True),
|
||||||
|
end=pd.to_datetime('2017-11-10', utc=True),
|
||||||
|
)
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
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, 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))
|
|
||||||
|
|
||||||
key = ','.join([exchange.name, symbol])
|
|
||||||
if key in self._asset_cache:
|
|
||||||
return self._asset_cache[key]
|
|
||||||
else:
|
|
||||||
asset = exchange.get_asset(symbol)
|
|
||||||
self._asset_cache[key] = asset
|
|
||||||
return asset
|
|
||||||
@@ -1,700 +0,0 @@
|
|||||||
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 = {}
|
|
||||||
self.load_assets()
|
|
||||||
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)
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
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.bundle = ExchangeBundle(self)
|
|
||||||
|
|
||||||
@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
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
|
|
||||||
# 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).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})
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from catalyst.data.bundles import register
|
|
||||||
from catalyst.exchange.exchange_bundle import exchange_bundle
|
|
||||||
|
|
||||||
symbols = (
|
|
||||||
'neo_btc',
|
|
||||||
)
|
|
||||||
register('exchange_bitfinex', exchange_bundle('bitfinex', symbols))
|
|
||||||
File diff suppressed because it is too large
Load Diff
+349
-269
@@ -5,26 +5,20 @@ 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 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, \
|
||||||
InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange, \
|
SymbolNotFoundOnExchange, \
|
||||||
PricingDataNotLoadedError, \
|
PricingDataNotLoadedError, \
|
||||||
NoDataAvailableOnExchange
|
NoDataAvailableOnExchange, NoValueForField, LastCandleTooEarlyError, \
|
||||||
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
|
TickerNotFoundError, NotEnoughCashError
|
||||||
ExchangeLimitOrder, ExchangeStopOrder
|
from catalyst.exchange.utils.datetime_utils import get_delta, \
|
||||||
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
get_periods_range, \
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols, \
|
get_periods, get_start_dt, get_frequency
|
||||||
get_frequency, resample_history_df
|
from catalyst.exchange.utils.exchange_utils import get_exchange_symbols, \
|
||||||
from catalyst.finance.order import ORDER_STATUS
|
resample_history_df, has_bundle
|
||||||
from catalyst.finance.transaction import Transaction
|
from logbook import Logger
|
||||||
from catalyst.utils.deprecate import deprecated
|
|
||||||
|
|
||||||
log = Logger('Exchange', level=LOG_LEVEL)
|
log = Logger('Exchange', level=LOG_LEVEL)
|
||||||
|
|
||||||
@@ -34,8 +28,8 @@ class Exchange:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.name = None
|
self.name = None
|
||||||
self.assets = {}
|
self.assets = []
|
||||||
self._portfolio = None
|
self._symbol_maps = [None, 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
|
||||||
@@ -43,28 +37,9 @@ class Exchange:
|
|||||||
self.num_candles_limit = None
|
self.num_candles_limit = None
|
||||||
self.max_requests_per_minute = None
|
self.max_requests_per_minute = None
|
||||||
self.request_cpt = None
|
self.request_cpt = None
|
||||||
self.bundle = ExchangeBundle(self)
|
self.bundle = ExchangeBundle(self.name)
|
||||||
|
|
||||||
@property
|
self.low_balance_threshold = None
|
||||||
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):
|
||||||
@@ -74,6 +49,9 @@ 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
|
||||||
@@ -132,7 +110,7 @@ class Exchange:
|
|||||||
|
|
||||||
def get_symbol(self, asset):
|
def get_symbol(self, asset):
|
||||||
"""
|
"""
|
||||||
The the exchange specific symbol of the specified market.
|
The exchange specific symbol of the specified market.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -145,9 +123,9 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
symbol = None
|
symbol = None
|
||||||
|
|
||||||
for key in self.assets:
|
for a in self.assets:
|
||||||
if not symbol and self.assets[key].symbol == asset.symbol:
|
if not symbol and a.symbol == asset.symbol:
|
||||||
symbol = key
|
symbol = a.symbol
|
||||||
|
|
||||||
if not symbol:
|
if not symbol:
|
||||||
raise ValueError('Currency %s not supported by exchange %s' %
|
raise ValueError('Currency %s not supported by exchange %s' %
|
||||||
@@ -174,67 +152,165 @@ class Exchange:
|
|||||||
|
|
||||||
return symbols
|
return symbols
|
||||||
|
|
||||||
def get_assets(self, symbols=None):
|
def get_assets(self, symbols=None, data_frequency=None,
|
||||||
|
is_exchange_symbol=False,
|
||||||
|
is_local=None, quote_currency=None):
|
||||||
"""
|
"""
|
||||||
The list of markets for the specified symbols.
|
The list of markets for the specified symbols.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
symbols: list[str]
|
symbols: list[str]
|
||||||
|
data_frequency: str
|
||||||
|
is_exchange_symbol: bool
|
||||||
|
is_local: bool
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
list[TradingPair]
|
list[TradingPair]
|
||||||
|
A list of asset objects.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
See get_asset for details of each parameter.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if symbols is None:
|
||||||
|
# Make a distinct list of all symbols
|
||||||
|
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
|
||||||
|
|
||||||
assets = []
|
assets = []
|
||||||
|
for symbol in symbols:
|
||||||
if symbols is not None:
|
try:
|
||||||
for symbol in symbols:
|
asset = self.get_asset(
|
||||||
asset = self.get_asset(symbol)
|
symbol, data_frequency, is_exchange_symbol, is_local
|
||||||
|
)
|
||||||
assets.append(asset)
|
assets.append(asset)
|
||||||
else:
|
|
||||||
for key in self.assets:
|
|
||||||
assets.append(self.assets[key])
|
|
||||||
|
|
||||||
|
except SymbolNotFoundOnExchange:
|
||||||
|
log.debug(
|
||||||
|
'skipping non-existent market {} {}'.format(
|
||||||
|
self.name, symbol
|
||||||
|
)
|
||||||
|
)
|
||||||
return assets
|
return assets
|
||||||
|
|
||||||
def get_asset(self, symbol):
|
def get_asset(self, symbol, data_frequency=None, is_exchange_symbol=False,
|
||||||
|
is_local=None):
|
||||||
"""
|
"""
|
||||||
The market for the specified symbol.
|
The market for the specified symbol.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
symbol: str
|
symbol: str
|
||||||
|
The Catalyst or exchange symbol.
|
||||||
|
|
||||||
|
data_frequency: str
|
||||||
|
Check for asset corresponding to the specified data_frequency.
|
||||||
|
The same asset might exist in the Catalyst repository or
|
||||||
|
locally (following a CSV ingestion). Filtering by
|
||||||
|
data_frequency picks the right asset.
|
||||||
|
|
||||||
|
is_exchange_symbol: bool
|
||||||
|
Whether the symbol uses the Catalyst or exchange convention.
|
||||||
|
|
||||||
|
is_local: bool
|
||||||
|
For the local or Catalyst asset.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
TradingPair
|
TradingPair
|
||||||
|
The asset object.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
asset = None
|
asset = None
|
||||||
|
|
||||||
for key in self.assets:
|
# TODO: temp mapping, fix to use a single symbol convention
|
||||||
if not asset and self.assets[key].symbol.lower() == symbol.lower():
|
og_symbol = symbol
|
||||||
asset = self.assets[key]
|
symbol = self.get_symbol(symbol) if not is_exchange_symbol else symbol
|
||||||
|
log.debug(
|
||||||
|
'searching assets for: {} {}'.format(
|
||||||
|
self.name, symbol
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# TODO: simplify and loose the loop
|
||||||
|
for a in self.assets:
|
||||||
|
if asset is not None:
|
||||||
|
break
|
||||||
|
|
||||||
if not asset:
|
if is_local is not None:
|
||||||
supported_symbols = [
|
data_source = 'local' if is_local else 'catalyst'
|
||||||
pair.symbol for pair in list(self.assets.values())
|
applies = (a.data_source == data_source)
|
||||||
]
|
|
||||||
|
elif data_frequency is not None:
|
||||||
|
applies = (
|
||||||
|
(
|
||||||
|
data_frequency == 'minute' and a.end_minute is not None)
|
||||||
|
or (
|
||||||
|
data_frequency == 'daily' and a.end_daily is not None)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
applies = True
|
||||||
|
|
||||||
|
# The symbol provided may use the Catalyst or the exchange
|
||||||
|
# convention
|
||||||
|
key = a.exchange_symbol if \
|
||||||
|
is_exchange_symbol else self.get_symbol(a)
|
||||||
|
if not asset and key.lower() == symbol.lower():
|
||||||
|
if applies:
|
||||||
|
asset = a
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise NoDataAvailableOnExchange(
|
||||||
|
symbol=key,
|
||||||
|
exchange=self.name,
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
)
|
||||||
|
|
||||||
|
if asset is None:
|
||||||
|
supported_symbols = sorted([a.symbol for a in self.assets])
|
||||||
|
|
||||||
raise SymbolNotFoundOnExchange(
|
raise SymbolNotFoundOnExchange(
|
||||||
symbol=symbol,
|
symbol=og_symbol,
|
||||||
exchange=self.name.title(),
|
exchange=self.name.title(),
|
||||||
supported_symbols=supported_symbols
|
supported_symbols=supported_symbols
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log.debug('found asset: {}'.format(asset))
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
def fetch_symbol_map(self):
|
def fetch_symbol_map(self, is_local=False):
|
||||||
return get_exchange_symbols(self.name)
|
index = 1 if is_local else 0
|
||||||
|
if self._symbol_maps[index] is not None:
|
||||||
|
return self._symbol_maps[index]
|
||||||
|
|
||||||
def load_assets(self):
|
else:
|
||||||
|
symbol_map = get_exchange_symbols(self.name, is_local)
|
||||||
|
self._symbol_maps[index] = symbol_map
|
||||||
|
return symbol_map
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def init(self):
|
||||||
|
"""
|
||||||
|
Load the asset list from the network.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def load_assets(self, is_local=False):
|
||||||
"""
|
"""
|
||||||
Populate the 'assets' attribute with a dictionary of Assets.
|
Populate the 'assets' attribute with a dictionary of Assets.
|
||||||
The key of the resulting dictionary is the exchange specific
|
The key of the resulting dictionary is the exchange specific
|
||||||
@@ -247,109 +323,11 @@ class Exchange:
|
|||||||
universal symbol. This simple approach avoids maintaining a mapping
|
universal symbol. This simple approach avoids maintaining a mapping
|
||||||
of sids.
|
of sids.
|
||||||
|
|
||||||
This method can be overridden if an exchange offers equivalent data
|
This method can be omerridden if an exchange offers equivalent data
|
||||||
via its api.
|
via its api.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
symbol_map = self.fetch_symbol_map()
|
pass
|
||||||
for exchange_symbol in symbol_map:
|
|
||||||
asset = symbol_map[exchange_symbol]
|
|
||||||
|
|
||||||
if 'start_date' in asset:
|
|
||||||
start_date = pd.to_datetime(asset['start_date'], utc=True)
|
|
||||||
else:
|
|
||||||
start_date = None
|
|
||||||
|
|
||||||
if 'end_date' in asset:
|
|
||||||
end_date = pd.to_datetime(asset['end_date'], utc=True)
|
|
||||||
else:
|
|
||||||
end_date = None
|
|
||||||
|
|
||||||
if 'leverage' in asset:
|
|
||||||
leverage = asset['leverage']
|
|
||||||
else:
|
|
||||||
leverage = 1.0
|
|
||||||
|
|
||||||
if 'asset_name' in asset:
|
|
||||||
asset_name = asset['asset_name']
|
|
||||||
else:
|
|
||||||
asset_name = None
|
|
||||||
|
|
||||||
if 'min_trade_size' in asset:
|
|
||||||
min_trade_size = asset['min_trade_size']
|
|
||||||
else:
|
|
||||||
min_trade_size = 0.0000001
|
|
||||||
|
|
||||||
if 'end_daily' in asset and asset['end_daily'] != 'N/A':
|
|
||||||
end_daily = pd.to_datetime(asset['end_daily'], utc=True)
|
|
||||||
else:
|
|
||||||
end_daily = None
|
|
||||||
|
|
||||||
if 'end_minute' in asset and asset['end_minute'] != 'N/A':
|
|
||||||
end_minute = pd.to_datetime(asset['end_minute'], utc=True)
|
|
||||||
else:
|
|
||||||
end_minute = None
|
|
||||||
|
|
||||||
trading_pair = TradingPair(
|
|
||||||
symbol=asset['symbol'],
|
|
||||||
exchange=self.name,
|
|
||||||
start_date=start_date,
|
|
||||||
end_date=end_date,
|
|
||||||
leverage=leverage,
|
|
||||||
asset_name=asset_name,
|
|
||||||
min_trade_size=min_trade_size,
|
|
||||||
end_daily=end_daily,
|
|
||||||
end_minute=end_minute,
|
|
||||||
exchange_symbol=exchange_symbol
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assets[exchange_symbol] = trading_pair
|
|
||||||
|
|
||||||
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'):
|
||||||
"""
|
"""
|
||||||
@@ -386,12 +364,15 @@ class Exchange:
|
|||||||
if field not in BASE_FIELDS:
|
if field not in BASE_FIELDS:
|
||||||
raise KeyError('Invalid column: {}'.format(field))
|
raise KeyError('Invalid column: {}'.format(field))
|
||||||
|
|
||||||
values = []
|
tickers = self.tickers(assets)
|
||||||
for asset in assets:
|
if field == 'close' or field == 'price':
|
||||||
value = self.get_single_spot_value(asset, field, data_frequency)
|
return [tickers[asset]['last'] for asset in tickers]
|
||||||
values.append(value)
|
|
||||||
|
|
||||||
return values
|
elif field == 'volume':
|
||||||
|
return [tickers[asset]['volume'] for asset in tickers]
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise NoValueForField(field=field)
|
||||||
|
|
||||||
def get_single_spot_value(self, asset, field, data_frequency):
|
def get_single_spot_value(self, asset, field, data_frequency):
|
||||||
"""
|
"""
|
||||||
@@ -433,6 +414,7 @@ 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):
|
||||||
"""
|
"""
|
||||||
@@ -457,7 +439,7 @@ class Exchange:
|
|||||||
series = pd.Series(values, index=dates)
|
series = pd.Series(values, index=dates)
|
||||||
|
|
||||||
periods = get_periods_range(
|
periods = get_periods_range(
|
||||||
start_dt, end_dt, data_frequency
|
start_dt=start_dt, end_dt=end_dt, freq=data_frequency
|
||||||
)
|
)
|
||||||
# TODO: ensure that this working as expected, if not use fillna
|
# TODO: ensure that this working as expected, if not use fillna
|
||||||
series = series.reindex(
|
series = series.reindex(
|
||||||
@@ -465,18 +447,17 @@ class Exchange:
|
|||||||
method='ffill',
|
method='ffill',
|
||||||
fill_value=previous_value,
|
fill_value=previous_value,
|
||||||
)
|
)
|
||||||
|
series.sort_index(inplace=True)
|
||||||
return series
|
return series
|
||||||
|
|
||||||
@deprecated
|
def get_history_window(self,
|
||||||
def get_history_window_direct(self,
|
assets,
|
||||||
assets,
|
end_dt,
|
||||||
end_dt,
|
bar_count,
|
||||||
bar_count,
|
frequency,
|
||||||
frequency,
|
field,
|
||||||
field,
|
data_frequency=None,
|
||||||
data_frequency=None,
|
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
|
||||||
@@ -503,10 +484,15 @@ 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.
|
||||||
|
|
||||||
# TODO: fill how?
|
is_current: bool
|
||||||
ffill: boolean
|
Skip date filters when current data is requested (last few bars
|
||||||
Forward-fill missing values. Only has effect if field
|
until now).
|
||||||
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
|
||||||
-------
|
-------
|
||||||
@@ -514,35 +500,56 @@ class Exchange:
|
|||||||
A dataframe containing the requested data.
|
A dataframe containing the requested data.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
start_dt = get_start_dt(end_dt, bar_count, data_frequency)
|
freq, candle_size, unit, data_frequency = get_frequency(
|
||||||
|
frequency, data_frequency
|
||||||
|
)
|
||||||
# The get_history method supports multiple asset
|
# The get_history method supports multiple asset
|
||||||
candles = self.get_candles(
|
candles = self.get_candles(
|
||||||
data_frequency=frequency,
|
freq=freq,
|
||||||
assets=assets,
|
assets=assets,
|
||||||
bar_count=bar_count,
|
bar_count=bar_count,
|
||||||
start_dt=start_dt,
|
end_dt=end_dt if not is_current else None,
|
||||||
end_dt=end_dt
|
|
||||||
)
|
|
||||||
candle_series = self.get_series_from_candles(
|
|
||||||
candles=candles,
|
|
||||||
start_dt=start_dt,
|
|
||||||
end_dt=end_dt,
|
|
||||||
data_frequency=frequency,
|
|
||||||
field=field,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
df = pd.DataFrame(candle_series)
|
series = dict()
|
||||||
|
for asset in candles:
|
||||||
|
first_candle = candles[asset][0]
|
||||||
|
asset_series = self.get_series_from_candles(
|
||||||
|
candles=candles[asset],
|
||||||
|
start_dt=first_candle['last_traded'],
|
||||||
|
end_dt=end_dt,
|
||||||
|
data_frequency=frequency,
|
||||||
|
field=field,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Checking to make sure that the dates match
|
||||||
|
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
|
||||||
|
|
||||||
|
df = pd.DataFrame(series)
|
||||||
|
df.dropna(inplace=True)
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def get_history_window(self,
|
def get_history_window_with_bundle(self,
|
||||||
assets,
|
assets,
|
||||||
end_dt,
|
end_dt,
|
||||||
bar_count,
|
bar_count,
|
||||||
frequency,
|
frequency,
|
||||||
field,
|
field,
|
||||||
data_frequency=None,
|
data_frequency=None,
|
||||||
ffill=True):
|
ffill=True,
|
||||||
|
force_auto_ingest=False):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Public API method that returns a dataframe containing the requested
|
Public API method that returns a dataframe containing the requested
|
||||||
@@ -580,6 +587,7 @@ class Exchange:
|
|||||||
A dataframe containing the requested data.
|
A dataframe containing the requested data.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# TODO: this function needs some work, we're currently using it just for benchmark data
|
||||||
freq, candle_size, unit, data_frequency = get_frequency(
|
freq, candle_size, unit, data_frequency = get_frequency(
|
||||||
frequency, data_frequency
|
frequency, data_frequency
|
||||||
)
|
)
|
||||||
@@ -590,8 +598,10 @@ class Exchange:
|
|||||||
end_dt=end_dt,
|
end_dt=end_dt,
|
||||||
bar_count=adj_bar_count,
|
bar_count=adj_bar_count,
|
||||||
field=field,
|
field=field,
|
||||||
data_frequency=data_frequency
|
data_frequency=data_frequency,
|
||||||
|
force_auto_ingest=force_auto_ingest
|
||||||
)
|
)
|
||||||
|
|
||||||
except (PricingDataNotLoadedError, NoDataAvailableOnExchange):
|
except (PricingDataNotLoadedError, NoDataAvailableOnExchange):
|
||||||
series = dict()
|
series = dict()
|
||||||
|
|
||||||
@@ -608,15 +618,14 @@ class Exchange:
|
|||||||
# The get_history method supports multiple asset
|
# The get_history method supports multiple asset
|
||||||
# Use the original frequency to let each api optimize
|
# Use the original frequency to let each api optimize
|
||||||
# the size of result sets
|
# the size of result sets
|
||||||
trailing_bar_count = get_periods(
|
trailing_bars = get_periods(
|
||||||
trailing_dt, end_dt, freq
|
trailing_dt, end_dt, freq
|
||||||
)
|
)
|
||||||
candles = self.get_candles(
|
candles = self.get_candles(
|
||||||
freq=freq,
|
freq=freq,
|
||||||
assets=asset,
|
assets=asset,
|
||||||
bar_count=trailing_bar_count,
|
end_dt=end_dt,
|
||||||
start_dt=start_dt,
|
bar_count=trailing_bars if trailing_bars < 500 else 500,
|
||||||
end_dt=end_dt
|
|
||||||
)
|
)
|
||||||
|
|
||||||
last_value = series[asset].iloc(0) if asset in series \
|
last_value = series[asset].iloc(0) if asset in series \
|
||||||
@@ -645,50 +654,100 @@ class Exchange:
|
|||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def synchronize_portfolio(self):
|
def _check_low_balance(self, currency, balances, amount):
|
||||||
|
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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
log.debug('synchronizing portfolio with exchange {}'.format(self.name))
|
free_cash = 0.0
|
||||||
balances = self.get_balances()
|
if check_balances:
|
||||||
|
log.debug('fetching {} balances'.format(self.name))
|
||||||
base_position_available = balances[self.base_currency] \
|
balances = self.get_balances()
|
||||||
if self.base_currency in balances else None
|
log.debug(
|
||||||
|
'got free balances for {} currencies'.format(
|
||||||
if base_position_available is None:
|
len(balances)
|
||||||
raise BaseCurrencyNotFoundError(
|
)
|
||||||
base_currency=self.base_currency,
|
|
||||||
exchange=self.name.title()
|
|
||||||
)
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
portfolio = self._portfolio
|
positions_value = 0.0
|
||||||
portfolio.cash = base_position_available
|
if positions is not None:
|
||||||
log.debug('found base currency balance: {}'.format(portfolio.cash))
|
assets = set([position.asset for position in positions])
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
portfolio.positions_value = 0.0
|
for position in positions:
|
||||||
for asset in tickers:
|
asset = position.asset
|
||||||
# TODO: convert if the position is not in the base currency
|
if asset not in tickers:
|
||||||
|
raise TickerNotFoundError(
|
||||||
|
symbol=asset.symbol,
|
||||||
|
exchange=self.name,
|
||||||
|
)
|
||||||
|
|
||||||
ticker = tickers[asset]
|
ticker = tickers[asset]
|
||||||
position = portfolio.positions[asset]
|
log.debug(
|
||||||
|
'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['timestamp']
|
position.last_sale_date = ticker['last_traded']
|
||||||
|
|
||||||
portfolio.positions_value += \
|
positions_value += \
|
||||||
position.amount * position.last_sale_price
|
position.amount * position.last_sale_price
|
||||||
portfolio.portfolio_value = \
|
|
||||||
portfolio.positions_value + portfolio.cash
|
|
||||||
|
|
||||||
def order(self, asset, amount, limit_price=None, stop_price=None,
|
if check_balances:
|
||||||
style=None):
|
free, is_lower = self._check_low_balance(
|
||||||
|
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
|
||||||
@@ -737,45 +796,30 @@ class Exchange:
|
|||||||
log.warn('skipping order amount of 0')
|
log.warn('skipping order amount of 0')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if asset.base_currency != self.base_currency.lower():
|
if self.base_currency is None:
|
||||||
|
raise ValueError('no base_currency defined for this exchange')
|
||||||
|
|
||||||
|
if asset.quote_currency != self.base_currency.lower():
|
||||||
raise MismatchingBaseCurrencies(
|
raise MismatchingBaseCurrencies(
|
||||||
base_currency=asset.base_currency,
|
base_currency=asset.quote_currency,
|
||||||
algo_currency=self.base_currency
|
algo_currency=self.base_currency
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
elif style is not None:
|
|
||||||
raise InvalidOrderStyle(exchange=self.name.title(),
|
|
||||||
style=style.__class__.__name__)
|
|
||||||
else:
|
|
||||||
raise ValueError('Incomplete order data.')
|
|
||||||
|
|
||||||
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}: {price}'.format(
|
'issuing {side} order of {amount} {symbol} for {type}:'
|
||||||
|
' {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,
|
||||||
type=style.__class__.__name__,
|
type=style.__class__.__name__,
|
||||||
price='{}{}'.format(display_price, asset.base_currency)
|
price='{}{}'.format(display_price, asset.quote_currency)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
order = self.create_order(asset, amount, is_buy, style)
|
|
||||||
if order:
|
return self.create_order(asset, amount, is_buy, style)
|
||||||
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
|
||||||
@@ -838,7 +882,7 @@ class Exchange:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_order(self, order_id):
|
def get_order(self, order_id, symbol_or_asset=None):
|
||||||
"""Lookup an order based on the order id returned from one of the
|
"""Lookup an order based on the order id returned from one of the
|
||||||
order functions.
|
order functions.
|
||||||
|
|
||||||
@@ -846,6 +890,8 @@ class Exchange:
|
|||||||
----------
|
----------
|
||||||
order_id : str
|
order_id : str
|
||||||
The unique identifier for the order.
|
The unique identifier for the order.
|
||||||
|
symbol_or_asset: str|TradingPair
|
||||||
|
The catalyst symbol, some exchanges need this
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -857,19 +903,36 @@ class Exchange:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def cancel_order(self, order_param):
|
def process_order(self, order):
|
||||||
|
"""
|
||||||
|
Similar to get_order but looks only for executed orders.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
order: Order
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
Avg execution price
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def cancel_order(self, order_param, symbol_or_asset=None):
|
||||||
"""Cancel an open order.
|
"""Cancel an open order.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
order_param : str or Order
|
order_param : str or Order
|
||||||
The order_id or order object to cancel.
|
The order_id or order object to cancel.
|
||||||
|
symbol_or_asset: str|TradingPair
|
||||||
|
The catalyst symbol, some exchanges need this
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_candles(self, freq, assets, bar_count=None,
|
def get_candles(self, freq, assets, bar_count, start_dt=None, end_dt=None):
|
||||||
start_dt=None, end_dt=None):
|
|
||||||
"""
|
"""
|
||||||
Retrieve OHLCV candles for the given assets
|
Retrieve OHLCV candles for the given assets
|
||||||
|
|
||||||
@@ -934,7 +997,7 @@ class Exchange:
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_orderbook(self, asset, order_type, limit):
|
def get_orderbook(self, asset, order_type, limit):
|
||||||
"""
|
"""
|
||||||
Retrieve the the orderbook for the given trading pair.
|
Retrieve the orderbook for the given trading pair.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -948,3 +1011,20 @@ class Exchange:
|
|||||||
list[dict[str, float]
|
list[dict[str, float]
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_trades(self, asset, my_trades, start_dt, limit):
|
||||||
|
"""
|
||||||
|
Retrieve a list of trades.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
my_trades: bool
|
||||||
|
List only my trades.
|
||||||
|
start_dt
|
||||||
|
limit
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
from catalyst.exchange.utils.factory import find_exchanges
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
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,21 +1,20 @@
|
|||||||
from catalyst.assets._assets import TradingPair
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
from redo import retry
|
||||||
|
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
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
|
from catalyst.finance.transaction import create_transaction, 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):
|
||||||
"""
|
"""
|
||||||
@@ -23,40 +22,55 @@ class TradingPairFeeSchedule(CommissionModel):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
fee : float, optional
|
maker : float, optional
|
||||||
The percentage fee.
|
The percentage maker fee.
|
||||||
|
|
||||||
|
taker: float, optional
|
||||||
|
The percentage taker fee.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self, maker=None, taker=None):
|
||||||
maker_fee=DEFAULT_MAKER_FEE,
|
self.maker = maker
|
||||||
taker_fee=DEFAULT_TAKER_FEE):
|
self.taker = taker
|
||||||
self.maker_fee = maker_fee
|
|
||||||
self.taker_fee = taker_fee
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
'{class_name}(maker_fee={maker_fee}, '
|
'{class_name}(maker={maker}, '
|
||||||
'taker_fee={taker_fee})'.format(
|
'taker={taker})'.format(
|
||||||
class_name=self.__class__.__name__,
|
class_name=self.__class__.__name__,
|
||||||
maker_fee=self.maker_fee,
|
maker=self.maker,
|
||||||
taker_fee=self.taker_fee,
|
taker=self.taker,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_maker_taker(self, asset):
|
||||||
|
maker = self.maker if self.maker is not None else asset.maker
|
||||||
|
taker = self.taker if self.taker is not None else asset.taker
|
||||||
|
return maker, taker
|
||||||
|
|
||||||
def calculate(self, order, transaction):
|
def calculate(self, order, transaction):
|
||||||
"""
|
"""
|
||||||
Calculate the final fee based on the order parameters.
|
Calculate the final fee based on the order parameters.
|
||||||
|
|
||||||
:param order:
|
:param order: Order
|
||||||
:param transaction:
|
:param transaction: Transaction
|
||||||
|
|
||||||
:return float:
|
:return float:
|
||||||
The total commission.
|
The total commission.
|
||||||
"""
|
"""
|
||||||
cost = abs(transaction.amount) * transaction.price
|
cost = abs(transaction.amount) * transaction.price
|
||||||
|
|
||||||
# Assuming just the taker fee for now
|
asset = order.asset
|
||||||
fee = cost * self.taker_fee
|
maker, taker = self.get_maker_taker(asset)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +84,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=DEFAULT_SLIPPAGE_SPREAD):
|
def __init__(self, spread=0.0001):
|
||||||
super(TradingPairFixedSlippage, self).__init__()
|
super(TradingPairFixedSlippage, self).__init__()
|
||||||
self.spread = spread
|
self.spread = spread
|
||||||
|
|
||||||
@@ -81,7 +95,6 @@ class TradingPairFixedSlippage(SlippageModel):
|
|||||||
|
|
||||||
def simulate(self, data, asset, orders_for_asset):
|
def simulate(self, data, asset, orders_for_asset):
|
||||||
self._volume_for_bar = 0
|
self._volume_for_bar = 0
|
||||||
|
|
||||||
price = data.current(asset, 'close')
|
price = data.current(asset, 'close')
|
||||||
|
|
||||||
dt = data.current_dt
|
dt = data.current_dt
|
||||||
@@ -91,18 +104,20 @@ class TradingPairFixedSlippage(SlippageModel):
|
|||||||
|
|
||||||
order.check_triggers(price, dt)
|
order.check_triggers(price, dt)
|
||||||
if not order.triggered:
|
if not order.triggered:
|
||||||
log.debug('order has not reached the trigger at current '
|
log.info(
|
||||||
'price {}'.format(price))
|
'order has not reached the trigger at current '
|
||||||
|
'price {}'.format(price)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
execution_price, execution_volume = self.process_order(data, order)
|
execution_price, execution_volume = self.process_order(data, order)
|
||||||
|
if execution_price is not None:
|
||||||
|
transaction = create_transaction(
|
||||||
|
order, dt, execution_price, execution_volume
|
||||||
|
)
|
||||||
|
|
||||||
transaction = create_transaction(
|
self._volume_for_bar += abs(transaction.amount)
|
||||||
order, dt, execution_price, execution_volume
|
yield order, transaction
|
||||||
)
|
|
||||||
|
|
||||||
self._volume_for_bar += abs(transaction.amount)
|
|
||||||
yield order, transaction
|
|
||||||
|
|
||||||
def process_order(self, data, order):
|
def process_order(self, data, order):
|
||||||
price = data.current(order.asset, 'close')
|
price = data.current(order.asset, 'close')
|
||||||
@@ -121,6 +136,15 @@ 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
|
||||||
@@ -132,3 +156,119 @@ 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))
|
||||||
|
|
||||||
|
transactions = exchange.process_order(order)
|
||||||
|
# This is a temporary measure, we should really update all
|
||||||
|
# trades, not just when the order gets filled. I just think
|
||||||
|
# that this is safer until we have a robust way to track
|
||||||
|
# the trades already processed by the algo. We can't loose
|
||||||
|
# them if the algo shuts down.
|
||||||
|
if transactions and order.open_amount == 0:
|
||||||
|
avg_price = np.average(
|
||||||
|
a=[t.price for t in transactions],
|
||||||
|
weights=[t.amount for t in transactions],
|
||||||
|
)
|
||||||
|
ostatus = 'filled' if order.open_amount == 0 else 'partial'
|
||||||
|
log.info(
|
||||||
|
'{} order {} / {}: {}, avg price: {}'.format(
|
||||||
|
ostatus,
|
||||||
|
order.id,
|
||||||
|
asset.symbol,
|
||||||
|
order.filled,
|
||||||
|
avg_price,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for transaction in transactions:
|
||||||
|
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,58 +1,37 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
from datetime import timedelta
|
||||||
|
from functools import partial
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
from operator import is_not
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from catalyst.assets._assets import TradingPair
|
import pytz
|
||||||
from logbook import Logger
|
|
||||||
from pandas.tslib import Timestamp
|
|
||||||
from pytz import UTC
|
|
||||||
from six import itervalues
|
|
||||||
|
|
||||||
from catalyst import get_calendar
|
from catalyst import get_calendar
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
|
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_delta, get_month_start_end, \
|
|
||||||
get_year_start_end, get_df_from_arrays, get_start_dt, get_period_label
|
|
||||||
from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \
|
from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \
|
||||||
BcolzExchangeBarWriter
|
BcolzExchangeBarWriter
|
||||||
from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \
|
from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \
|
||||||
TempBundleNotFoundError, \
|
TempBundleNotFoundError, \
|
||||||
NoDataAvailableOnExchange, \
|
NoDataAvailableOnExchange, \
|
||||||
PricingDataNotLoadedError
|
PricingDataNotLoadedError, DataCorruptionError, PricingDataValueError
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_folder
|
from catalyst.exchange.utils.bundle_utils import range_in_bundle, \
|
||||||
|
get_bcolz_chunk, get_df_from_arrays, get_assets
|
||||||
|
from catalyst.exchange.utils.datetime_utils import get_delta, get_start_dt, \
|
||||||
|
get_period_label, get_month_start_end, get_year_start_end
|
||||||
|
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
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
from pandas.tslib import Timestamp
|
|
||||||
from pytz import UTC
|
from pytz import UTC
|
||||||
from six import itervalues
|
from six import itervalues
|
||||||
|
|
||||||
from catalyst import get_calendar
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.data.minute_bars import BcolzMinuteOverlappingData, \
|
|
||||||
BcolzMinuteBarMetadata
|
|
||||||
from catalyst.exchange.bundle_utils import range_in_bundle, \
|
|
||||||
get_bcolz_chunk, get_delta, get_month_start_end, \
|
|
||||||
get_year_start_end, get_df_from_arrays, get_start_dt, get_period_label
|
|
||||||
from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \
|
|
||||||
BcolzExchangeBarWriter
|
|
||||||
from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \
|
|
||||||
TempBundleNotFoundError, \
|
|
||||||
NoDataAvailableOnExchange, \
|
|
||||||
PricingDataNotLoadedError
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_folder
|
|
||||||
from catalyst.utils.cli import maybe_show_progress
|
|
||||||
from catalyst.utils.paths import ensure_directory
|
|
||||||
|
|
||||||
log = Logger('exchange_bundle', level=LOG_LEVEL)
|
log = Logger('exchange_bundle', level=LOG_LEVEL)
|
||||||
|
|
||||||
BUNDLE_NAME_TEMPLATE = os.path.join('{root}', '{frequency}_bundle')
|
BUNDLE_NAME_TEMPLATE = os.path.join('{root}', '{frequency}_bundle')
|
||||||
@@ -63,23 +42,14 @@ def _cachpath(symbol, type_):
|
|||||||
|
|
||||||
|
|
||||||
class ExchangeBundle:
|
class ExchangeBundle:
|
||||||
def __init__(self, exchange):
|
def __init__(self, exchange_name):
|
||||||
self.exchange = exchange
|
self.exchange_name = exchange_name
|
||||||
self.minutes_per_day = 1440
|
self.minutes_per_day = 1440
|
||||||
self.default_ohlc_ratio = 1000000
|
self.default_ohlc_ratio = 1000000
|
||||||
self._writers = dict()
|
self._writers = dict()
|
||||||
self._readers = dict()
|
self._readers = dict()
|
||||||
self.calendar = get_calendar('OPEN')
|
self.calendar = get_calendar('OPEN')
|
||||||
|
self.exchange = None
|
||||||
def get_assets(self, include_symbols, exclude_symbols):
|
|
||||||
# TODO: filter exclude symbols assets
|
|
||||||
if include_symbols is not None:
|
|
||||||
include_symbols_list = include_symbols.split(',')
|
|
||||||
|
|
||||||
return self.exchange.get_assets(include_symbols_list)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return self.exchange.get_assets()
|
|
||||||
|
|
||||||
def get_reader(self, data_frequency, path=None):
|
def get_reader(self, data_frequency, path=None):
|
||||||
"""
|
"""
|
||||||
@@ -91,7 +61,7 @@ class ExchangeBundle:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if path is None:
|
if path is None:
|
||||||
root = get_exchange_folder(self.exchange.name)
|
root = get_exchange_folder(self.exchange_name)
|
||||||
path = BUNDLE_NAME_TEMPLATE.format(
|
path = BUNDLE_NAME_TEMPLATE.format(
|
||||||
root=root,
|
root=root,
|
||||||
frequency=data_frequency
|
frequency=data_frequency
|
||||||
@@ -122,7 +92,7 @@ class ExchangeBundle:
|
|||||||
BcolzMinuteBarWriter | BcolzDailyBarWriter
|
BcolzMinuteBarWriter | BcolzDailyBarWriter
|
||||||
|
|
||||||
"""
|
"""
|
||||||
root = get_exchange_folder(self.exchange.name)
|
root = get_exchange_folder(self.exchange_name)
|
||||||
path = BUNDLE_NAME_TEMPLATE.format(
|
path = BUNDLE_NAME_TEMPLATE.format(
|
||||||
root=root,
|
root=root,
|
||||||
frequency=data_frequency
|
frequency=data_frequency
|
||||||
@@ -180,9 +150,9 @@ class ExchangeBundle:
|
|||||||
----------
|
----------
|
||||||
assets: list[TradingPair]
|
assets: list[TradingPair]
|
||||||
The assets is scope.
|
The assets is scope.
|
||||||
start_dt: datetime
|
start_dt: pd.Timestamp
|
||||||
The chunk start date.
|
The chunk start date.
|
||||||
end_dt: datetime
|
end_dt: pd.Timestamp
|
||||||
The chunk end date.
|
The chunk end date.
|
||||||
data_frequency: str
|
data_frequency: str
|
||||||
|
|
||||||
@@ -231,8 +201,8 @@ class ExchangeBundle:
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
start_dt: datetime
|
start_dt: pd.Timestamp
|
||||||
end_dt: datetime
|
end_dt: pd.Timestamp
|
||||||
data_frequency: str
|
data_frequency: str
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
@@ -244,8 +214,91 @@ class ExchangeBundle:
|
|||||||
if data_frequency == 'minute' \
|
if data_frequency == 'minute' \
|
||||||
else self.calendar.sessions_in_range(start_dt, end_dt)
|
else self.calendar.sessions_in_range(start_dt, end_dt)
|
||||||
|
|
||||||
|
def _spot_empty_periods(self, ohlcv_df, asset, data_frequency,
|
||||||
|
empty_rows_behavior):
|
||||||
|
problems = []
|
||||||
|
|
||||||
|
nan_rows = ohlcv_df[ohlcv_df.isnull().T.any().T].index
|
||||||
|
if len(nan_rows) > 0:
|
||||||
|
dates = []
|
||||||
|
for row_date in nan_rows.values:
|
||||||
|
row_date = pd.to_datetime(row_date, utc=True)
|
||||||
|
if row_date > asset.start_date:
|
||||||
|
dates.append(row_date)
|
||||||
|
|
||||||
|
if len(dates) > 0:
|
||||||
|
end_dt = asset.end_minute if data_frequency == 'minute' \
|
||||||
|
else asset.end_daily
|
||||||
|
|
||||||
|
problem = '{name} ({start_dt} to {end_dt}) has empty ' \
|
||||||
|
'periods: {dates}'.format(
|
||||||
|
name=asset.symbol,
|
||||||
|
start_dt=asset.start_date.strftime(
|
||||||
|
DATE_TIME_FORMAT),
|
||||||
|
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
||||||
|
dates=[date.strftime(
|
||||||
|
DATE_TIME_FORMAT) for date in dates])
|
||||||
|
|
||||||
|
if empty_rows_behavior == 'warn':
|
||||||
|
log.warn(problem)
|
||||||
|
|
||||||
|
elif empty_rows_behavior == 'raise':
|
||||||
|
raise EmptyValuesInBundleError(
|
||||||
|
name=asset.symbol,
|
||||||
|
end_minute=end_dt,
|
||||||
|
dates=dates, )
|
||||||
|
|
||||||
|
else:
|
||||||
|
ohlcv_df.dropna(inplace=True)
|
||||||
|
|
||||||
|
else:
|
||||||
|
problem = None
|
||||||
|
|
||||||
|
problems.append(problem)
|
||||||
|
|
||||||
|
return problems
|
||||||
|
|
||||||
|
def _spot_duplicates(self, ohlcv_df, asset, data_frequency, threshold):
|
||||||
|
# TODO: work in progress
|
||||||
|
series = ohlcv_df.reset_index().groupby('close')['index'].apply(
|
||||||
|
np.array
|
||||||
|
)
|
||||||
|
|
||||||
|
ref_delta = timedelta(minutes=1) if data_frequency == 'minute' \
|
||||||
|
else timedelta(days=1)
|
||||||
|
|
||||||
|
dups = series.loc[lambda values: [len(x) > 10 for x in values]]
|
||||||
|
|
||||||
|
for index, dates in dups.iteritems():
|
||||||
|
prev_date = None
|
||||||
|
for date in dates:
|
||||||
|
if prev_date is not None:
|
||||||
|
delta = (date - prev_date) / 1e9
|
||||||
|
if delta == ref_delta.seconds:
|
||||||
|
log.info('pex')
|
||||||
|
|
||||||
|
prev_date = date
|
||||||
|
|
||||||
|
problems = []
|
||||||
|
for index, dates in dups.iteritems():
|
||||||
|
end_dt = asset.end_minute if data_frequency == 'minute' \
|
||||||
|
else asset.end_daily
|
||||||
|
|
||||||
|
problem = '{name} ({start_dt} to {end_dt}) has {threshold} ' \
|
||||||
|
'identical close values on: {dates}'.format(
|
||||||
|
name=asset.symbol,
|
||||||
|
start_dt=asset.start_date.strftime(DATE_TIME_FORMAT),
|
||||||
|
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
||||||
|
threshold=threshold,
|
||||||
|
dates=[pd.to_datetime(date).strftime(DATE_TIME_FORMAT)
|
||||||
|
for date in dates])
|
||||||
|
|
||||||
|
problems.append(problem)
|
||||||
|
|
||||||
|
return problems
|
||||||
|
|
||||||
def ingest_df(self, ohlcv_df, data_frequency, asset, writer,
|
def ingest_df(self, ohlcv_df, data_frequency, asset, writer,
|
||||||
empty_rows_behavior='strip'):
|
empty_rows_behavior='warn', duplicates_threshold=None):
|
||||||
"""
|
"""
|
||||||
Ingest a DataFrame of OHLCV data for a given market.
|
Ingest a DataFrame of OHLCV data for a given market.
|
||||||
|
|
||||||
@@ -258,50 +311,16 @@ class ExchangeBundle:
|
|||||||
empty_rows_behavior: str
|
empty_rows_behavior: str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
problems = []
|
||||||
if empty_rows_behavior is not 'ignore':
|
if empty_rows_behavior is not 'ignore':
|
||||||
nan_rows = ohlcv_df[ohlcv_df.isnull().T.any().T].index
|
problems += self._spot_empty_periods(
|
||||||
|
ohlcv_df, asset, data_frequency, empty_rows_behavior
|
||||||
|
)
|
||||||
|
|
||||||
if len(nan_rows) > 0:
|
# if duplicates_threshold is not None:
|
||||||
dates = []
|
# problems += self._spot_duplicates(
|
||||||
previous_date = None
|
# ohlcv_df, asset, data_frequency, duplicates_threshold
|
||||||
for row_date in nan_rows.values:
|
# )
|
||||||
row_date = pd.to_datetime(row_date)
|
|
||||||
|
|
||||||
if previous_date is None:
|
|
||||||
dates.append(row_date)
|
|
||||||
|
|
||||||
else:
|
|
||||||
seq_date = previous_date + get_delta(1, data_frequency)
|
|
||||||
|
|
||||||
if row_date > seq_date:
|
|
||||||
dates.append(previous_date)
|
|
||||||
dates.append(row_date)
|
|
||||||
|
|
||||||
previous_date = row_date
|
|
||||||
|
|
||||||
dates.append(pd.to_datetime(nan_rows.values[-1]))
|
|
||||||
|
|
||||||
name = '{} from {} to {}'.format(
|
|
||||||
asset.symbol, ohlcv_df.index[0], ohlcv_df.index[-1]
|
|
||||||
)
|
|
||||||
if empty_rows_behavior == 'warn':
|
|
||||||
log.warn(
|
|
||||||
'\n{name} with end minute {end_minute} has empty rows '
|
|
||||||
'in ranges: {dates}'.format(
|
|
||||||
name=name,
|
|
||||||
end_minute=asset.end_minute,
|
|
||||||
dates=dates
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif empty_rows_behavior == 'raise':
|
|
||||||
raise EmptyValuesInBundleError(
|
|
||||||
name=name,
|
|
||||||
end_minute=asset.end_minute,
|
|
||||||
dates=dates
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
ohlcv_df.dropna(inplace=True)
|
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
if not ohlcv_df.empty:
|
if not ohlcv_df.empty:
|
||||||
@@ -310,8 +329,11 @@ class ExchangeBundle:
|
|||||||
|
|
||||||
self._write(data, writer, data_frequency)
|
self._write(data, writer, data_frequency)
|
||||||
|
|
||||||
|
return problems
|
||||||
|
|
||||||
def ingest_ctable(self, asset, data_frequency, period,
|
def ingest_ctable(self, asset, data_frequency, period,
|
||||||
writer, empty_rows_behavior='strip', cleanup=False):
|
writer, empty_rows_behavior='strip',
|
||||||
|
duplicates_threshold=100, cleanup=False):
|
||||||
"""
|
"""
|
||||||
Merge a ctable bundle chunk into the main bundle for the exchange.
|
Merge a ctable bundle chunk into the main bundle for the exchange.
|
||||||
|
|
||||||
@@ -327,11 +349,17 @@ class ExchangeBundle:
|
|||||||
cleanup: bool
|
cleanup: bool
|
||||||
Remove the temp bundle directory after ingestion.
|
Remove the temp bundle directory after ingestion.
|
||||||
|
|
||||||
:return:
|
Returns
|
||||||
|
-------
|
||||||
|
list[str]
|
||||||
|
A list of problems which occurred during ingestion.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
problems = []
|
||||||
|
|
||||||
# Download and extract the bundle
|
# Download and extract the bundle
|
||||||
path = get_bcolz_chunk(
|
path = get_bcolz_chunk(
|
||||||
exchange_name=self.exchange.name,
|
exchange_name=self.exchange_name,
|
||||||
symbol=asset.symbol,
|
symbol=asset.symbol,
|
||||||
data_frequency=data_frequency,
|
data_frequency=data_frequency,
|
||||||
period=period
|
period=period
|
||||||
@@ -375,12 +403,13 @@ class ExchangeBundle:
|
|||||||
start_dt, end_dt, data_frequency
|
start_dt, end_dt, data_frequency
|
||||||
)
|
)
|
||||||
df = get_df_from_arrays(arrays, periods)
|
df = get_df_from_arrays(arrays, periods)
|
||||||
self.ingest_df(
|
problems += self.ingest_df(
|
||||||
ohlcv_df=df,
|
ohlcv_df=df,
|
||||||
data_frequency=data_frequency,
|
data_frequency=data_frequency,
|
||||||
asset=asset,
|
asset=asset,
|
||||||
writer=writer,
|
writer=writer,
|
||||||
empty_rows_behavior=empty_rows_behavior
|
empty_rows_behavior=empty_rows_behavior,
|
||||||
|
duplicates_threshold=duplicates_threshold
|
||||||
)
|
)
|
||||||
|
|
||||||
if cleanup:
|
if cleanup:
|
||||||
@@ -390,7 +419,7 @@ class ExchangeBundle:
|
|||||||
)
|
)
|
||||||
shutil.rmtree(reader._rootdir)
|
shutil.rmtree(reader._rootdir)
|
||||||
|
|
||||||
return reader._rootdir
|
return filter(partial(is_not, None), problems)
|
||||||
|
|
||||||
def get_adj_dates(self, start, end, assets, data_frequency):
|
def get_adj_dates(self, start, end, assets, data_frequency):
|
||||||
"""
|
"""
|
||||||
@@ -399,14 +428,14 @@ class ExchangeBundle:
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
start: datetime
|
start: pd.Timestamp
|
||||||
end: datetime
|
end: pd.Timestamp
|
||||||
assets: list[TradingPair]
|
assets: list[TradingPair]
|
||||||
data_frequency: str
|
data_frequency: str
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
datetime, datetime
|
pd.Timestamp, pd.Timestamp
|
||||||
"""
|
"""
|
||||||
earliest_trade = None
|
earliest_trade = None
|
||||||
last_entry = None
|
last_entry = None
|
||||||
@@ -432,8 +461,9 @@ 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 end is None or (last_entry is not None and end > last_entry):
|
if last_entry is not None and (end is None or end > last_entry):
|
||||||
end = last_entry
|
end = last_entry.replace(minute=59, hour=23) \
|
||||||
|
if data_frequency == 'minute' else last_entry
|
||||||
|
|
||||||
if end is None or start is None or start > end:
|
if end is None or start is None or start > end:
|
||||||
raise NoDataAvailableOnExchange(
|
raise NoDataAvailableOnExchange(
|
||||||
@@ -453,8 +483,8 @@ class ExchangeBundle:
|
|||||||
----------
|
----------
|
||||||
assets: list[TradingPair]
|
assets: list[TradingPair]
|
||||||
data_frequency: str
|
data_frequency: str
|
||||||
start_dt: datetime
|
start_dt: pd.Timestamp
|
||||||
end_dt: datetime
|
end_dt: pd.Timestamp
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -528,7 +558,8 @@ class ExchangeBundle:
|
|||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
def ingest_assets(self, assets, data_frequency, start_dt=None, end_dt=None,
|
def ingest_assets(self, assets, data_frequency, start_dt=None, end_dt=None,
|
||||||
show_progress=False, asset_chunks=False):
|
show_progress=False, show_breakdown=False,
|
||||||
|
show_report=False):
|
||||||
"""
|
"""
|
||||||
Determine if data is missing from the bundle and attempt to ingest it.
|
Determine if data is missing from the bundle and attempt to ingest it.
|
||||||
|
|
||||||
@@ -536,10 +567,10 @@ class ExchangeBundle:
|
|||||||
----------
|
----------
|
||||||
assets: list[TradingPair]
|
assets: list[TradingPair]
|
||||||
data_frequency: str
|
data_frequency: str
|
||||||
start_dt: datetime
|
start_dt: pd.Timestamp
|
||||||
end_dt: datetime
|
end_dt: pd.Timestamp
|
||||||
show_progress: bool
|
show_progress: bool
|
||||||
asset_chunks: bool
|
show_breakdown: bool
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if start_dt is None:
|
if start_dt is None:
|
||||||
@@ -562,22 +593,23 @@ class ExchangeBundle:
|
|||||||
end_dt=end_dt
|
end_dt=end_dt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
problems = []
|
||||||
# This is the common writer for the entire exchange bundle
|
# This is the common writer for the entire exchange bundle
|
||||||
# we want to give an end_date far in time
|
# we want to give an end_date far in time
|
||||||
writer = self.get_writer(start_dt, end_dt, data_frequency)
|
writer = self.get_writer(start_dt, end_dt, data_frequency)
|
||||||
if asset_chunks:
|
if show_breakdown:
|
||||||
for asset in chunks:
|
for asset in chunks:
|
||||||
with maybe_show_progress(
|
with maybe_show_progress(
|
||||||
chunks[asset],
|
chunks[asset],
|
||||||
show_progress,
|
show_progress,
|
||||||
label='Ingesting {frequency} price data for '
|
label='Ingesting {frequency} price data for '
|
||||||
'{symbol} on {exchange}'.format(
|
'{symbol} on {exchange}'.format(
|
||||||
exchange=self.exchange.name,
|
exchange=self.exchange_name,
|
||||||
frequency=data_frequency,
|
frequency=data_frequency,
|
||||||
symbol=asset.symbol
|
symbol=asset.symbol
|
||||||
)) as it:
|
)) as it:
|
||||||
for chunk in it:
|
for chunk in it:
|
||||||
self.ingest_ctable(
|
problems += self.ingest_ctable(
|
||||||
asset=chunk['asset'],
|
asset=chunk['asset'],
|
||||||
data_frequency=data_frequency,
|
data_frequency=data_frequency,
|
||||||
period=chunk['period'],
|
period=chunk['period'],
|
||||||
@@ -597,11 +629,11 @@ class ExchangeBundle:
|
|||||||
show_progress,
|
show_progress,
|
||||||
label='Ingesting {frequency} price data on '
|
label='Ingesting {frequency} price data on '
|
||||||
'{exchange}'.format(
|
'{exchange}'.format(
|
||||||
exchange=self.exchange.name,
|
exchange=self.exchange_name,
|
||||||
frequency=data_frequency,
|
frequency=data_frequency,
|
||||||
)) as it:
|
)) as it:
|
||||||
for chunk in it:
|
for chunk in it:
|
||||||
self.ingest_ctable(
|
problems += self.ingest_ctable(
|
||||||
asset=chunk['asset'],
|
asset=chunk['asset'],
|
||||||
data_frequency=data_frequency,
|
data_frequency=data_frequency,
|
||||||
period=chunk['period'],
|
period=chunk['period'],
|
||||||
@@ -610,9 +642,150 @@ class ExchangeBundle:
|
|||||||
cleanup=True
|
cleanup=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if show_report and len(problems) > 0:
|
||||||
|
log.info('problems during ingestion:{}\n'.format(
|
||||||
|
'\n'.join(problems)
|
||||||
|
))
|
||||||
|
|
||||||
|
def ingest_csv(self, path, data_frequency, empty_rows_behavior='strip',
|
||||||
|
duplicates_threshold=100):
|
||||||
|
"""
|
||||||
|
Ingest price data from a CSV file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path: str
|
||||||
|
data_frequency: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[str]
|
||||||
|
A list of potential problems detected during ingestion.
|
||||||
|
|
||||||
|
"""
|
||||||
|
log.info('ingesting csv file: {}'.format(path))
|
||||||
|
|
||||||
|
if self.exchange is None:
|
||||||
|
# Avoid circular dependencies
|
||||||
|
from catalyst.exchange.utils.factory import get_exchange
|
||||||
|
self.exchange = get_exchange(self.exchange_name)
|
||||||
|
|
||||||
|
problems = []
|
||||||
|
df = pd.read_csv(
|
||||||
|
path,
|
||||||
|
header=0,
|
||||||
|
sep=',',
|
||||||
|
dtype=dict(
|
||||||
|
symbol=np.object_,
|
||||||
|
last_traded=np.object_,
|
||||||
|
open=np.float64,
|
||||||
|
high=np.float64,
|
||||||
|
low=np.float64,
|
||||||
|
close=np.float64,
|
||||||
|
volume=np.float64
|
||||||
|
),
|
||||||
|
parse_dates=['last_traded'],
|
||||||
|
index_col=None
|
||||||
|
)
|
||||||
|
min_start_dt = None
|
||||||
|
max_end_dt = None
|
||||||
|
|
||||||
|
symbols = df['symbol'].unique()
|
||||||
|
|
||||||
|
# Apply the timezone before creating an index for simplicity
|
||||||
|
df['last_traded'] = df['last_traded'].dt.tz_localize(pytz.UTC)
|
||||||
|
df.set_index(['symbol', 'last_traded'], drop=True, inplace=True)
|
||||||
|
|
||||||
|
assets = dict()
|
||||||
|
for symbol in symbols:
|
||||||
|
start_dt = df.index.get_level_values(1).min()
|
||||||
|
end_dt = df.index.get_level_values(1).max()
|
||||||
|
end_dt_key = 'end_{}'.format(data_frequency)
|
||||||
|
|
||||||
|
market = self.exchange.get_market(symbol)
|
||||||
|
if market is None:
|
||||||
|
raise ValueError('symbol not available in the exchange.')
|
||||||
|
|
||||||
|
params = dict(
|
||||||
|
exchange=self.exchange.name,
|
||||||
|
data_source='local',
|
||||||
|
exchange_symbol=market['id'],
|
||||||
|
)
|
||||||
|
mixin_market_params(self.exchange_name, params, market)
|
||||||
|
|
||||||
|
asset_def = self.exchange.get_asset_def(market, True)
|
||||||
|
if asset_def is not None:
|
||||||
|
params['symbol'] = asset_def['symbol']
|
||||||
|
|
||||||
|
params['start_date'] = asset_def['start_date'] \
|
||||||
|
if asset_def['start_date'] < start_dt else start_dt
|
||||||
|
|
||||||
|
params['end_date'] = asset_def[end_dt_key] \
|
||||||
|
if asset_def[end_dt_key] > end_dt else end_dt
|
||||||
|
|
||||||
|
params['end_daily'] = end_dt \
|
||||||
|
if data_frequency == 'daily' else asset_def['end_daily']
|
||||||
|
|
||||||
|
params['end_minute'] = end_dt \
|
||||||
|
if data_frequency == 'minute' else asset_def['end_minute']
|
||||||
|
|
||||||
|
else:
|
||||||
|
params['symbol'] = get_catalyst_symbol(market)
|
||||||
|
|
||||||
|
params['end_daily'] = end_dt \
|
||||||
|
if data_frequency == 'daily' else 'N/A'
|
||||||
|
params['end_minute'] = end_dt \
|
||||||
|
if data_frequency == 'minute' else 'N/A'
|
||||||
|
|
||||||
|
if min_start_dt is None or start_dt < min_start_dt:
|
||||||
|
min_start_dt = start_dt
|
||||||
|
|
||||||
|
if max_end_dt is None or end_dt > max_end_dt:
|
||||||
|
max_end_dt = end_dt
|
||||||
|
|
||||||
|
asset = TradingPair(**params)
|
||||||
|
assets[market['id']] = asset
|
||||||
|
|
||||||
|
save_exchange_symbols(self.exchange_name, assets, True)
|
||||||
|
|
||||||
|
writer = self.get_writer(
|
||||||
|
start_dt=min_start_dt.replace(hour=00, minute=00),
|
||||||
|
end_dt=max_end_dt.replace(hour=23, minute=59),
|
||||||
|
data_frequency=data_frequency
|
||||||
|
)
|
||||||
|
|
||||||
|
for symbol in assets:
|
||||||
|
# here the symbol is the market['id']
|
||||||
|
asset = assets[symbol]
|
||||||
|
ohlcv_df = df.loc[
|
||||||
|
(df.index.get_level_values(0) == asset.symbol)
|
||||||
|
] # type: pd.DataFrame
|
||||||
|
ohlcv_df.index = ohlcv_df.index.droplevel(0)
|
||||||
|
|
||||||
|
period_start = start_dt.replace(hour=00, minute=00)
|
||||||
|
period_end = end_dt.replace(hour=23, minute=59)
|
||||||
|
periods = self.get_calendar_periods_range(
|
||||||
|
period_start, period_end, data_frequency
|
||||||
|
)
|
||||||
|
|
||||||
|
# We're not really resampling but ensuring that each frame
|
||||||
|
# contains data
|
||||||
|
ohlcv_df = ohlcv_df.reindex(periods, method='ffill')
|
||||||
|
ohlcv_df['volume'] = ohlcv_df['volume'].fillna(0)
|
||||||
|
|
||||||
|
problems += self.ingest_df(
|
||||||
|
ohlcv_df=ohlcv_df,
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
asset=asset,
|
||||||
|
writer=writer,
|
||||||
|
empty_rows_behavior=empty_rows_behavior,
|
||||||
|
duplicates_threshold=duplicates_threshold
|
||||||
|
)
|
||||||
|
return filter(partial(is_not, None), problems)
|
||||||
|
|
||||||
def ingest(self, data_frequency, include_symbols=None,
|
def ingest(self, data_frequency, include_symbols=None,
|
||||||
exclude_symbols=None, start=None, end=None,
|
exclude_symbols=None, start=None, end=None, csv=None,
|
||||||
show_progress=True, environ=os.environ):
|
show_progress=True, show_breakdown=True, show_report=True):
|
||||||
"""
|
"""
|
||||||
Inject data based on specified parameters.
|
Inject data based on specified parameters.
|
||||||
|
|
||||||
@@ -621,17 +794,34 @@ class ExchangeBundle:
|
|||||||
data_frequency: str
|
data_frequency: str
|
||||||
include_symbols: str
|
include_symbols: str
|
||||||
exclude_symbols: str
|
exclude_symbols: str
|
||||||
start: datetime
|
start: pd.Timestamp
|
||||||
end: datetime
|
end: pd.Timestamp
|
||||||
show_progress: bool
|
show_progress: bool
|
||||||
environ:
|
environ:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
assets = self.get_assets(include_symbols, exclude_symbols)
|
if csv is not None:
|
||||||
|
self.ingest_csv(csv, data_frequency)
|
||||||
|
|
||||||
for frequency in data_frequency.split(','):
|
else:
|
||||||
self.ingest_assets(assets, frequency, start, end,
|
if self.exchange is None:
|
||||||
show_progress, True)
|
# Avoid circular dependencies
|
||||||
|
from catalyst.exchange.utils.factory import get_exchange
|
||||||
|
self.exchange = get_exchange(self.exchange_name)
|
||||||
|
|
||||||
|
assets = get_assets(
|
||||||
|
self.exchange, include_symbols, exclude_symbols
|
||||||
|
)
|
||||||
|
for frequency in data_frequency.split(','):
|
||||||
|
self.ingest_assets(
|
||||||
|
assets=assets,
|
||||||
|
data_frequency=frequency,
|
||||||
|
start_dt=start,
|
||||||
|
end_dt=end,
|
||||||
|
show_progress=show_progress,
|
||||||
|
show_breakdown=show_breakdown,
|
||||||
|
show_report=show_report
|
||||||
|
)
|
||||||
|
|
||||||
def get_history_window_series_and_load(self,
|
def get_history_window_series_and_load(self,
|
||||||
assets,
|
assets,
|
||||||
@@ -639,7 +829,9 @@ class ExchangeBundle:
|
|||||||
bar_count,
|
bar_count,
|
||||||
field,
|
field,
|
||||||
data_frequency,
|
data_frequency,
|
||||||
algo_end_dt=None
|
algo_end_dt=None,
|
||||||
|
trailing_bar_count=None,
|
||||||
|
force_auto_ingest=False
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Retrieve price data history, ingest missing data.
|
Retrieve price data history, ingest missing data.
|
||||||
@@ -647,55 +839,69 @@ class ExchangeBundle:
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
assets: list[TradingPair]
|
assets: list[TradingPair]
|
||||||
end_dt: datetime
|
end_dt: pd.Timestamp
|
||||||
bar_count: int
|
bar_count: int
|
||||||
field: str
|
field: str
|
||||||
data_frequency: str
|
data_frequency: str
|
||||||
algo_end_dt: datetime
|
algo_end_dt: pd.Timestamp
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Series
|
Series
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
if AUTO_INGEST or force_auto_ingest:
|
||||||
|
try:
|
||||||
|
series = self.get_history_window_series(
|
||||||
|
assets=assets,
|
||||||
|
end_dt=end_dt,
|
||||||
|
bar_count=bar_count,
|
||||||
|
field=field,
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
trailing_bar_count=trailing_bar_count,
|
||||||
|
)
|
||||||
|
return pd.DataFrame(series)
|
||||||
|
|
||||||
|
except PricingDataNotLoadedError:
|
||||||
|
start_dt = get_start_dt(end_dt, bar_count, data_frequency)
|
||||||
|
log.info(
|
||||||
|
'pricing data for {symbol} not found in range '
|
||||||
|
'{start} to {end}, updating the bundles.'.format(
|
||||||
|
symbol=[asset.symbol for asset in assets],
|
||||||
|
start=start_dt,
|
||||||
|
end=end_dt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.ingest_assets(
|
||||||
|
assets=assets,
|
||||||
|
start_dt=start_dt,
|
||||||
|
end_dt=algo_end_dt, # TODO: apply trailing bars
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
show_progress=True,
|
||||||
|
show_breakdown=True
|
||||||
|
)
|
||||||
|
series = self.get_history_window_series(
|
||||||
|
assets=assets,
|
||||||
|
end_dt=end_dt,
|
||||||
|
bar_count=bar_count,
|
||||||
|
field=field,
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
reset_reader=True,
|
||||||
|
trailing_bar_count=trailing_bar_count,
|
||||||
|
)
|
||||||
|
return series
|
||||||
|
|
||||||
|
else:
|
||||||
series = self.get_history_window_series(
|
series = self.get_history_window_series(
|
||||||
assets=assets,
|
assets=assets,
|
||||||
end_dt=end_dt,
|
end_dt=end_dt,
|
||||||
bar_count=bar_count,
|
bar_count=bar_count,
|
||||||
field=field,
|
field=field,
|
||||||
data_frequency=data_frequency
|
data_frequency=data_frequency,
|
||||||
|
trailing_bar_count=trailing_bar_count,
|
||||||
)
|
)
|
||||||
return pd.DataFrame(series)
|
return pd.DataFrame(series)
|
||||||
|
|
||||||
except PricingDataNotLoadedError:
|
|
||||||
start_dt = get_start_dt(end_dt, bar_count, data_frequency)
|
|
||||||
log.info(
|
|
||||||
'pricing data for {symbol} not found in range '
|
|
||||||
'{start} to {end}, updating the bundles.'.format(
|
|
||||||
symbol=[asset.symbol for asset in assets],
|
|
||||||
start=start_dt,
|
|
||||||
end=end_dt
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.ingest_assets(
|
|
||||||
assets=assets,
|
|
||||||
start_dt=start_dt,
|
|
||||||
end_dt=algo_end_dt,
|
|
||||||
data_frequency=data_frequency,
|
|
||||||
show_progress=True,
|
|
||||||
asset_chunks=True
|
|
||||||
)
|
|
||||||
series = self.get_history_window_series(
|
|
||||||
assets=assets,
|
|
||||||
end_dt=end_dt,
|
|
||||||
bar_count=bar_count,
|
|
||||||
field=field,
|
|
||||||
data_frequency=data_frequency,
|
|
||||||
reset_reader=False
|
|
||||||
)
|
|
||||||
return series
|
|
||||||
|
|
||||||
def get_spot_values(self,
|
def get_spot_values(self,
|
||||||
assets,
|
assets,
|
||||||
field,
|
field,
|
||||||
@@ -707,12 +913,18 @@ class ExchangeBundle:
|
|||||||
The spot values for the gives assets, field and date. Reads from
|
The spot values for the gives assets, field and date. Reads from
|
||||||
the exchange data bundle.
|
the exchange data bundle.
|
||||||
|
|
||||||
:param assets:
|
Parameters
|
||||||
:param field:
|
----------
|
||||||
:param dt:
|
assets: list[TradingPair]
|
||||||
:param data_frequency:
|
field: str
|
||||||
:param reset_reader:
|
dt: pd.Timestamp
|
||||||
:return:
|
data_frequency: str
|
||||||
|
reset_reader:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
"""
|
"""
|
||||||
values = []
|
values = []
|
||||||
try:
|
try:
|
||||||
@@ -736,10 +948,12 @@ class ExchangeBundle:
|
|||||||
raise PricingDataNotLoadedError(
|
raise PricingDataNotLoadedError(
|
||||||
field=field,
|
field=field,
|
||||||
first_trading_day=min([asset.start_date for asset in assets]),
|
first_trading_day=min([asset.start_date for asset in assets]),
|
||||||
exchange=self.exchange.name,
|
exchange=self.exchange_name,
|
||||||
symbols=symbols,
|
symbols=symbols,
|
||||||
symbol_list=','.join(symbols),
|
symbol_list=','.join(symbols),
|
||||||
data_frequency=data_frequency
|
data_frequency=data_frequency,
|
||||||
|
start_dt=dt,
|
||||||
|
end_dt=dt
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_history_window_series(self,
|
def get_history_window_series(self,
|
||||||
@@ -748,12 +962,20 @@ class ExchangeBundle:
|
|||||||
bar_count,
|
bar_count,
|
||||||
field,
|
field,
|
||||||
data_frequency,
|
data_frequency,
|
||||||
|
trailing_bar_count=None,
|
||||||
reset_reader=False):
|
reset_reader=False):
|
||||||
start_dt = get_start_dt(end_dt, bar_count, data_frequency)
|
if trailing_bar_count:
|
||||||
start_dt, end_dt = self.get_adj_dates(
|
delta = get_delta(trailing_bar_count, data_frequency)
|
||||||
|
end_dt += delta
|
||||||
|
|
||||||
|
start_dt = get_start_dt(end_dt, bar_count, data_frequency, False)
|
||||||
|
start_dt, _ = self.get_adj_dates(
|
||||||
start_dt, end_dt, assets, data_frequency
|
start_dt, end_dt, assets, data_frequency
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# This is an attempt to resolve some caching with the reader
|
||||||
|
# when auto-ingesting data.
|
||||||
|
# TODO: needs more work
|
||||||
reader = self.get_reader(data_frequency)
|
reader = self.get_reader(data_frequency)
|
||||||
if reset_reader:
|
if reset_reader:
|
||||||
del self._readers[reader._rootdir]
|
del self._readers[reader._rootdir]
|
||||||
@@ -764,59 +986,69 @@ class ExchangeBundle:
|
|||||||
raise PricingDataNotLoadedError(
|
raise PricingDataNotLoadedError(
|
||||||
field=field,
|
field=field,
|
||||||
first_trading_day=min([asset.start_date for asset in assets]),
|
first_trading_day=min([asset.start_date for asset in assets]),
|
||||||
exchange=self.exchange.name,
|
exchange=self.exchange_name,
|
||||||
symbols=symbols,
|
symbols=symbols,
|
||||||
symbol_list=','.join(symbols),
|
symbol_list=','.join(symbols),
|
||||||
data_frequency=data_frequency
|
data_frequency=data_frequency,
|
||||||
|
start_dt=start_dt,
|
||||||
|
end_dt=end_dt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
series = dict()
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
asset_start_dt, asset_end_dt = self.get_adj_dates(
|
asset_start_dt, _ = self.get_adj_dates(
|
||||||
start_dt, end_dt, assets, data_frequency
|
start_dt, end_dt, assets, data_frequency
|
||||||
)
|
)
|
||||||
|
|
||||||
in_bundle = range_in_bundle(
|
in_bundle = range_in_bundle(
|
||||||
asset, asset_start_dt, asset_end_dt, reader
|
asset, asset_start_dt, end_dt, reader
|
||||||
)
|
)
|
||||||
if not in_bundle:
|
if not in_bundle:
|
||||||
raise PricingDataNotLoadedError(
|
raise PricingDataNotLoadedError(
|
||||||
field=field,
|
field=field,
|
||||||
first_trading_day=asset.start_date,
|
first_trading_day=asset.start_date,
|
||||||
exchange=self.exchange.name,
|
exchange=self.exchange_name,
|
||||||
symbols=asset.symbol,
|
symbols=asset.symbol,
|
||||||
symbol_list=asset.symbol,
|
symbol_list=asset.symbol,
|
||||||
data_frequency=data_frequency
|
data_frequency=data_frequency,
|
||||||
|
start_dt=asset_start_dt,
|
||||||
|
end_dt=end_dt
|
||||||
)
|
)
|
||||||
|
|
||||||
series = dict()
|
periods = self.get_calendar_periods_range(
|
||||||
try:
|
asset_start_dt, end_dt, data_frequency
|
||||||
|
)
|
||||||
|
# This does not behave well when requesting multiple assets
|
||||||
|
# when the start or end date of one asset is outside of the range
|
||||||
|
# looking at the logic in load_raw_arrays(), we are not achieving
|
||||||
|
# any performance gain by requesting multiple sids at once. It's
|
||||||
|
# looping through the sids and making separate requests anyway.
|
||||||
arrays = reader.load_raw_arrays(
|
arrays = reader.load_raw_arrays(
|
||||||
sids=[asset.sid for asset in assets],
|
sids=[asset.sid],
|
||||||
fields=[field],
|
fields=[field],
|
||||||
start_dt=start_dt,
|
start_dt=start_dt,
|
||||||
end_dt=end_dt
|
end_dt=end_dt
|
||||||
)
|
)
|
||||||
|
if len(arrays) == 0:
|
||||||
|
raise DataCorruptionError(
|
||||||
|
exchange=self.exchange_name,
|
||||||
|
symbols=asset.symbol,
|
||||||
|
start_dt=asset_start_dt,
|
||||||
|
end_dt=end_dt
|
||||||
|
)
|
||||||
|
|
||||||
except Exception:
|
field_values = arrays[0][:, 0]
|
||||||
symbols = [asset.symbol.encode('utf-8') for asset in assets]
|
|
||||||
raise PricingDataNotLoadedError(
|
|
||||||
field=field,
|
|
||||||
first_trading_day=min([asset.start_date for asset in assets]),
|
|
||||||
exchange=self.exchange.name,
|
|
||||||
symbols=symbols,
|
|
||||||
symbol_list=','.join(symbols),
|
|
||||||
data_frequency=data_frequency
|
|
||||||
)
|
|
||||||
|
|
||||||
periods = self.get_calendar_periods_range(
|
try:
|
||||||
start_dt, end_dt, data_frequency
|
value_series = pd.Series(field_values, index=periods)
|
||||||
)
|
series[asset] = value_series
|
||||||
|
except ValueError as e:
|
||||||
for asset_index, asset in enumerate(assets):
|
raise PricingDataValueError(
|
||||||
asset_values = arrays[asset_index]
|
exchange=asset.exchange,
|
||||||
|
symbol=asset.symbol,
|
||||||
value_series = pd.Series(asset_values.flatten(), index=periods)
|
start_dt=asset_start_dt,
|
||||||
series[asset] = value_series
|
end_dt=end_dt,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
return series
|
return series
|
||||||
|
|
||||||
@@ -830,14 +1062,18 @@ class ExchangeBundle:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
log.debug('cleaning exchange {}, frequency {}'.format(
|
log.debug('cleaning exchange {}, frequency {}'.format(
|
||||||
self.exchange.name, data_frequency
|
self.exchange_name, data_frequency
|
||||||
))
|
))
|
||||||
root = get_exchange_folder(self.exchange.name)
|
root = get_exchange_folder(self.exchange_name)
|
||||||
|
|
||||||
symbols = os.path.join(root, 'symbols.json')
|
symbols = os.path.join(root, 'symbols.json')
|
||||||
if os.path.isfile(symbols):
|
if os.path.isfile(symbols):
|
||||||
os.remove(symbols)
|
os.remove(symbols)
|
||||||
|
|
||||||
|
local_symbols = os.path.join(root, 'symbols_local.json')
|
||||||
|
if os.path.isfile(local_symbols):
|
||||||
|
os.remove(local_symbols)
|
||||||
|
|
||||||
temp_bundles = os.path.join(root, 'temp_bundles')
|
temp_bundles = os.path.join(root, 'temp_bundles')
|
||||||
|
|
||||||
if os.path.isdir(temp_bundles):
|
if os.path.isdir(temp_bundles):
|
||||||
|
|||||||
@@ -1,31 +1,29 @@
|
|||||||
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 catalyst.constants import LOG_LEVEL, AUTO_INGEST
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
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.exchange_utils import get_frequency, resample_history_df
|
from catalyst.exchange.utils.exchange_utils import resample_history_df, group_assets_by_exchange
|
||||||
|
from catalyst.exchange.utils.datetime_utils import get_frequency
|
||||||
|
from logbook import Logger
|
||||||
|
from redo import retry
|
||||||
|
|
||||||
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(
|
||||||
self.exchanges = kwargs.pop('exchanges', None)
|
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)
|
||||||
|
|
||||||
@@ -36,41 +34,15 @@ class DataPortalExchangeBase(DataPortal):
|
|||||||
frequency,
|
frequency,
|
||||||
field,
|
field,
|
||||||
data_frequency,
|
data_frequency,
|
||||||
ffill=True,
|
ffill=True):
|
||||||
attempt_index=0):
|
exchange_assets = group_assets_by_exchange(assets)
|
||||||
try:
|
if len(exchange_assets) > 1:
|
||||||
exchange_assets = dict()
|
df_list = []
|
||||||
for asset in assets:
|
for exchange_name in exchange_assets:
|
||||||
if asset.exchange not in exchange_assets:
|
assets = exchange_assets[exchange_name]
|
||||||
exchange_assets[asset.exchange] = list()
|
|
||||||
|
|
||||||
exchange_assets[asset.exchange].append(asset)
|
df_exchange = self.get_exchange_history_window(
|
||||||
|
exchange_name,
|
||||||
if len(exchange_assets) > 1:
|
|
||||||
df_list = []
|
|
||||||
for exchange_name in exchange_assets:
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
assets = exchange_assets[exchange_name]
|
|
||||||
|
|
||||||
df_exchange = self.get_exchange_history_window(
|
|
||||||
exchange,
|
|
||||||
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 = self.exchanges[list(exchange_assets.keys())[0]]
|
|
||||||
return self.get_exchange_history_window(
|
|
||||||
exchange,
|
|
||||||
assets,
|
assets,
|
||||||
end_dt,
|
end_dt,
|
||||||
bar_count,
|
bar_count,
|
||||||
@@ -79,26 +51,22 @@ class DataPortalExchangeBase(DataPortal):
|
|||||||
data_frequency,
|
data_frequency,
|
||||||
ffill)
|
ffill)
|
||||||
|
|
||||||
except ExchangeRequestError as e:
|
df_list.append(df_exchange)
|
||||||
log.warn(
|
|
||||||
'get history attempt {}: {}'.format(attempt_index, e)
|
# Merging the values values of each exchange
|
||||||
)
|
return pd.concat(df_list)
|
||||||
if attempt_index < self.retry_get_history_window:
|
|
||||||
sleep(self.retry_delay)
|
else:
|
||||||
return self._get_history_window(assets,
|
exchange_name = list(exchange_assets.keys())[0]
|
||||||
end_dt,
|
return self.get_exchange_history_window(
|
||||||
bar_count,
|
exchange_name,
|
||||||
frequency,
|
assets,
|
||||||
field,
|
end_dt,
|
||||||
data_frequency,
|
bar_count,
|
||||||
ffill,
|
frequency,
|
||||||
attempt_index + 1)
|
field,
|
||||||
else:
|
data_frequency,
|
||||||
raise ExchangeBarDataError(
|
ffill)
|
||||||
data_type='history',
|
|
||||||
attempts=attempt_index,
|
|
||||||
error=e
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_history_window(self,
|
def get_history_window(self,
|
||||||
assets,
|
assets,
|
||||||
@@ -112,17 +80,23 @@ class DataPortalExchangeBase(DataPortal):
|
|||||||
if field == 'price':
|
if field == 'price':
|
||||||
field = 'close'
|
field = 'close'
|
||||||
|
|
||||||
return self._get_history_window(assets,
|
return retry(
|
||||||
end_dt,
|
action=self._get_history_window,
|
||||||
bar_count,
|
attempts=self.attempts['get_history_window_attempts'],
|
||||||
frequency,
|
sleeptime=self.attempts['retry_sleeptime'],
|
||||||
field,
|
retry_exceptions=(ExchangeRequestError,),
|
||||||
data_frequency,
|
cleanup=lambda: log.warn('fetching history again.'),
|
||||||
ffill)
|
args=(assets,
|
||||||
|
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,
|
||||||
exchange,
|
exchange_name,
|
||||||
assets,
|
assets,
|
||||||
end_dt,
|
end_dt,
|
||||||
bar_count,
|
bar_count,
|
||||||
@@ -132,74 +106,61 @@ 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):
|
||||||
attempt_index=0):
|
if isinstance(assets, TradingPair):
|
||||||
try:
|
spot_values = self.get_exchange_spot_value(
|
||||||
if isinstance(assets, TradingPair):
|
assets.exchange, [assets], field, dt, data_frequency)
|
||||||
exchange = self.exchanges[assets.exchange]
|
|
||||||
spot_values = self.get_exchange_spot_value(
|
|
||||||
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:
|
||||||
exchange_assets = dict()
|
spot_values = []
|
||||||
for asset in assets:
|
for exchange_name in exchange_assets:
|
||||||
if asset.exchange not in exchange_assets:
|
assets = exchange_assets[exchange_name]
|
||||||
exchange_assets[asset.exchange] = list()
|
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
|
||||||
|
|
||||||
exchange_assets[asset.exchange].append(asset)
|
return spot_values
|
||||||
|
|
||||||
if len(list(exchange_assets.keys())) == 1:
|
|
||||||
exchange = self.exchanges[list(exchange_assets.keys())[0]]
|
|
||||||
return self.get_exchange_spot_value(
|
|
||||||
exchange, assets, field, dt, data_frequency)
|
|
||||||
|
|
||||||
else:
|
|
||||||
spot_values = []
|
|
||||||
for exchange_name in exchange_assets:
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
assets = exchange_assets[exchange_name]
|
|
||||||
exchange_spot_values = self.get_exchange_spot_value(
|
|
||||||
exchange,
|
|
||||||
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 self._get_spot_value(assets, field, dt, data_frequency)
|
return retry(
|
||||||
|
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, assets, field, dt,
|
def get_exchange_spot_value(self, exchange_name, assets, field, dt,
|
||||||
data_frequency):
|
data_frequency):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -214,10 +175,11 @@ class DataPortalExchangeBase(DataPortal):
|
|||||||
|
|
||||||
class DataPortalExchangeLive(DataPortalExchangeBase):
|
class DataPortalExchangeLive(DataPortalExchangeBase):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.exchanges = kwargs.pop('exchanges', None)
|
||||||
super(DataPortalExchangeLive, self).__init__(*args, **kwargs)
|
super(DataPortalExchangeLive, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_exchange_history_window(self,
|
def get_exchange_history_window(self,
|
||||||
exchange,
|
exchange_name,
|
||||||
assets,
|
assets,
|
||||||
end_dt,
|
end_dt,
|
||||||
bar_count,
|
bar_count,
|
||||||
@@ -230,7 +192,7 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
exchange: Exchange
|
exchange_name: Exchange
|
||||||
assets: list[TradingPair]
|
assets: list[TradingPair]
|
||||||
end_dt: datetime
|
end_dt: datetime
|
||||||
bar_count: int
|
bar_count: int
|
||||||
@@ -244,6 +206,8 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
|
|||||||
DataFrame
|
DataFrame
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
exchange = self.exchanges[exchange_name]
|
||||||
|
|
||||||
df = exchange.get_history_window(
|
df = exchange.get_history_window(
|
||||||
assets,
|
assets,
|
||||||
end_dt,
|
end_dt,
|
||||||
@@ -251,17 +215,17 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
|
|||||||
frequency,
|
frequency,
|
||||||
field,
|
field,
|
||||||
data_frequency,
|
data_frequency,
|
||||||
ffill)
|
False)
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def get_exchange_spot_value(self, exchange, assets, field, dt,
|
def get_exchange_spot_value(self, exchange_name, assets, field, dt,
|
||||||
data_frequency):
|
data_frequency):
|
||||||
"""
|
"""
|
||||||
A spot value for the exchange.
|
A spot value for the exchange.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
exchange: Exchange
|
exchange_name: str
|
||||||
assets: list[TradingPair]
|
assets: list[TradingPair]
|
||||||
field: str
|
field: str
|
||||||
dt: datetime
|
dt: datetime
|
||||||
@@ -272,6 +236,7 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
|
|||||||
float
|
float
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
exchange = self.exchanges[exchange_name]
|
||||||
exchange_spot_values = exchange.get_spot_value(
|
exchange_spot_values = exchange.get_spot_value(
|
||||||
assets, field, dt, data_frequency)
|
assets, field, dt, data_frequency)
|
||||||
|
|
||||||
@@ -280,16 +245,16 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
|
|||||||
|
|
||||||
class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.exchange_names = kwargs.pop('exchange_names', None)
|
||||||
|
|
||||||
super(DataPortalExchangeBacktest, self).__init__(*args, **kwargs)
|
super(DataPortalExchangeBacktest, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.exchange_bundles = dict()
|
self.exchange_bundles = dict()
|
||||||
|
|
||||||
self.history_loaders = dict()
|
self.history_loaders = dict()
|
||||||
self.minute_history_loaders = dict()
|
self.minute_history_loaders = dict()
|
||||||
|
|
||||||
for exchange_name in self.exchanges:
|
for name in self.exchange_names:
|
||||||
exchange = self.exchanges[exchange_name]
|
self.exchange_bundles[name] = ExchangeBundle(name)
|
||||||
self.exchange_bundles[exchange_name] = ExchangeBundle(exchange)
|
|
||||||
|
|
||||||
def _get_first_trading_day(self, assets):
|
def _get_first_trading_day(self, assets):
|
||||||
first_date = None
|
first_date = None
|
||||||
@@ -299,7 +264,7 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
|||||||
return first_date
|
return first_date
|
||||||
|
|
||||||
def get_exchange_history_window(self,
|
def get_exchange_history_window(self,
|
||||||
exchange,
|
exchange_name,
|
||||||
assets,
|
assets,
|
||||||
end_dt,
|
end_dt,
|
||||||
bar_count,
|
bar_count,
|
||||||
@@ -326,12 +291,14 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
|||||||
DataFrame
|
DataFrame
|
||||||
|
|
||||||
"""
|
"""
|
||||||
bundle = self.exchange_bundles[exchange.name] # type: ExchangeBundle
|
# TODO: verify that the exchange supports the timeframe
|
||||||
|
bundle = self.exchange_bundles[exchange_name] # type: ExchangeBundle
|
||||||
|
|
||||||
freq, candle_size, unit, adj_data_frequency = get_frequency(
|
freq, candle_size, unit, adj_data_frequency = get_frequency(
|
||||||
frequency, data_frequency
|
frequency, data_frequency
|
||||||
)
|
)
|
||||||
adj_bar_count = candle_size * bar_count
|
adj_bar_count = candle_size * bar_count
|
||||||
|
trailing_bar_count = candle_size - 1
|
||||||
|
|
||||||
if data_frequency == 'minute' and adj_data_frequency == 'daily':
|
if data_frequency == 'minute' and adj_data_frequency == 'daily':
|
||||||
end_dt = end_dt.floor('1D')
|
end_dt = end_dt.floor('1D')
|
||||||
@@ -343,13 +310,14 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
|||||||
field=field,
|
field=field,
|
||||||
data_frequency=adj_data_frequency,
|
data_frequency=adj_data_frequency,
|
||||||
algo_end_dt=self._last_available_session,
|
algo_end_dt=self._last_available_session,
|
||||||
|
trailing_bar_count=trailing_bar_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
df = resample_history_df(pd.DataFrame(series), freq, field)
|
df = resample_history_df(pd.DataFrame(series), freq, field)
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def get_exchange_spot_value(self,
|
def get_exchange_spot_value(self,
|
||||||
exchange,
|
exchange_name,
|
||||||
assets,
|
assets,
|
||||||
field,
|
field,
|
||||||
dt,
|
dt,
|
||||||
@@ -361,7 +329,7 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
exchange: Exchange
|
exchange_name: str
|
||||||
assets: list[TradingPair]
|
assets: list[TradingPair]
|
||||||
field: str
|
field: str
|
||||||
dt: datetime
|
dt: datetime
|
||||||
@@ -372,30 +340,34 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
|||||||
float
|
float
|
||||||
|
|
||||||
"""
|
"""
|
||||||
bundle = self.exchange_bundles[exchange.name]
|
bundle = self.exchange_bundles[exchange_name]
|
||||||
if data_frequency == 'daily':
|
if data_frequency == 'daily':
|
||||||
dt = dt.floor('1D')
|
dt = dt.floor('1D')
|
||||||
else:
|
else:
|
||||||
dt = dt.floor('1 min')
|
dt = dt.floor('1 min')
|
||||||
|
|
||||||
try:
|
if AUTO_INGEST:
|
||||||
return bundle.get_spot_values(assets, field, dt, data_frequency)
|
try:
|
||||||
|
return bundle.get_spot_values(
|
||||||
except PricingDataNotLoadedError:
|
assets, field, dt, data_frequency
|
||||||
log.info(
|
|
||||||
'pricing data for {symbol} not found on {dt}'
|
|
||||||
', updating the bundles.'.format(
|
|
||||||
symbol=[asset.symbol for asset in assets],
|
|
||||||
dt=dt
|
|
||||||
)
|
)
|
||||||
)
|
except PricingDataNotLoadedError:
|
||||||
bundle.ingest_assets(
|
log.info(
|
||||||
assets=assets,
|
'pricing data for {symbol} not found on {dt}'
|
||||||
start_dt=self._first_trading_day,
|
', updating the bundles.'.format(
|
||||||
end_dt=self._last_available_session,
|
symbol=[asset.symbol for asset in assets],
|
||||||
data_frequency=data_frequency,
|
dt=dt
|
||||||
show_progress=True
|
)
|
||||||
)
|
)
|
||||||
return bundle.get_spot_values(
|
bundle.ingest_assets(
|
||||||
assets, field, dt, data_frequency, True
|
assets=assets,
|
||||||
)
|
start_dt=self._first_trading_day,
|
||||||
|
end_dt=self._last_available_session,
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
show_progress=True
|
||||||
|
)
|
||||||
|
return bundle.get_spot_values(
|
||||||
|
assets, field, dt, data_frequency, True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return bundle.get_spot_values(assets, field, dt, data_frequency)
|
||||||
|
|||||||
@@ -100,6 +100,19 @@ class InvalidHistoryFrequencyError(ZiplineError):
|
|||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedHistoryFrequencyError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'{exchange} does not support candle frequency {freq}, please choose '
|
||||||
|
'from: {freqs}.'
|
||||||
|
).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 '
|
||||||
@@ -143,7 +156,8 @@ class OrphanOrderError(ZiplineError):
|
|||||||
|
|
||||||
class OrphanOrderReverseError(ZiplineError):
|
class OrphanOrderReverseError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Order {order_id} tracked by algorithm, but not found in exchange {exchange}.'
|
'Order {order_id} tracked by algorithm, but not found in exchange '
|
||||||
|
'{exchange}.'
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -161,8 +175,8 @@ class SidHashError(ZiplineError):
|
|||||||
|
|
||||||
class BaseCurrencyNotFoundError(ZiplineError):
|
class BaseCurrencyNotFoundError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Algorithm base currency {base_currency} not found in exchange '
|
'Algorithm base currency {base_currency} not found in account '
|
||||||
'{exchange}.'
|
'balances on {exchange}: {balances}'
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -206,25 +220,105 @@ 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 or '
|
'starts on {first_trading_day}, but you are either trying to trade '
|
||||||
'retrieve pricing data on {dt}. Adjust your dates accordingly.').strip()
|
'or retrieve pricing data on {dt}. Adjust your dates accordingly.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class PricingDataNotLoadedError(ZiplineError):
|
class PricingDataNotLoadedError(ZiplineError):
|
||||||
msg = ('Pricing data {field} for trading pairs {symbols} trading on '
|
msg = ('Missing data for {exchange} {symbols} in date range '
|
||||||
'exchange {exchange} since {first_trading_day} is unavailable. '
|
'[{start_dt} - {end_dt}]'
|
||||||
'The bundle data is either out-of-date or has not been loaded yet. '
|
'\nPlease run: `catalyst ingest-exchange -x {exchange} -f '
|
||||||
'Please ingest data using the command '
|
'{data_frequency} -i {symbol_list}`. See catalyst documentation '
|
||||||
'`catalyst ingest-exchange -x {exchange} -f {data_frequency} -i {symbol_list}`. '
|
'for details.').strip()
|
||||||
'See catalyst documentation for details.').strip()
|
|
||||||
|
|
||||||
|
class PricingDataValueError(ZiplineError):
|
||||||
|
msg = ('Unable to retrieve pricing data for {exchange} {symbol} '
|
||||||
|
'[{start_dt} - {end_dt}]: {error}').strip()
|
||||||
|
|
||||||
|
|
||||||
|
class DataCorruptionError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Unable to validate data for {exchange} {symbols} in date range '
|
||||||
|
'[{start_dt} - {end_dt}]. The data is either corrupted or '
|
||||||
|
'unavailable. Please try deleting this bundle:'
|
||||||
|
'\n`catalyst clean-exchange -x {exchange}\n'
|
||||||
|
'Then, ingest the data again. Please contact the Catalyst team if '
|
||||||
|
'the issue persists.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class ApiCandlesError(ZiplineError):
|
class ApiCandlesError(ZiplineError):
|
||||||
msg = ('Unable to fetch candles from the remote API: {error}.').strip()
|
msg = (
|
||||||
|
'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 exchange {exchange} '
|
'Requested data for trading pair {symbol} is not available on '
|
||||||
|
'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.').strip()
|
'Check `http://enigma.co/catalyst/status` for market coverage.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class NoValueForField(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'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,9 +1,7 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.protocol import Portfolio, Positions, Position
|
from catalyst.protocol import Portfolio, Positions, Position
|
||||||
from catalyst.utils.deprecate import deprecated
|
from logbook import Logger
|
||||||
|
|
||||||
log = Logger('ExchangePortfolio', level=LOG_LEVEL)
|
log = Logger('ExchangePortfolio', level=LOG_LEVEL)
|
||||||
|
|
||||||
@@ -11,7 +9,8 @@ log = Logger('ExchangePortfolio', level=LOG_LEVEL)
|
|||||||
class ExchangePortfolio(Portfolio):
|
class ExchangePortfolio(Portfolio):
|
||||||
"""
|
"""
|
||||||
Since the goal is to support multiple exchanges, it makes sense to
|
Since the goal is to support multiple exchanges, it makes sense to
|
||||||
include additional stats in the portfolio object.
|
include additional stats in the portfolio object. This fills the role
|
||||||
|
of Blotter and Portfolio in live mode.
|
||||||
|
|
||||||
Instead of relying on the performance tracker, each exchange portfolio
|
Instead of relying on the performance tracker, each exchange portfolio
|
||||||
tracks its own holding. This offers a separation between tracking an
|
tracks its own holding. This offers a separation between tracking an
|
||||||
@@ -40,7 +39,13 @@ 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
|
||||||
@@ -52,6 +57,17 @@ 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.
|
||||||
@@ -66,14 +82,15 @@ class ExchangePortfolio(Portfolio):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
log.debug('executing order {}'.format(order.id))
|
log.debug('executing order {}'.format(order.id))
|
||||||
del self.open_orders[order.id]
|
self._remove_open_order(order)
|
||||||
|
|
||||||
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: %s' % order.id
|
'Trying to execute order for a position not held:'
|
||||||
|
' {}'.format(order.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.capital_used += order.amount * transaction.price
|
self.capital_used += order.amount * transaction.price
|
||||||
@@ -89,32 +106,6 @@ class ExchangePortfolio(Portfolio):
|
|||||||
|
|
||||||
log.debug('updated portfolio with executed order')
|
log.debug('updated portfolio with executed order')
|
||||||
|
|
||||||
@deprecated
|
|
||||||
def execute_transaction(self, transaction):
|
|
||||||
# TODO: almost duplicate of execute_order. Not sure why Poloniex needs this.
|
|
||||||
log.debug('executing transaction {}'.format(transaction.order_id))
|
|
||||||
|
|
||||||
order_position = self.positions[transaction.asset] \
|
|
||||||
if transaction.asset in self.positions else None
|
|
||||||
|
|
||||||
if order_position is None:
|
|
||||||
raise ValueError(
|
|
||||||
'Trying to execute transaction for a position not held: %s' % transaction.order_id
|
|
||||||
)
|
|
||||||
|
|
||||||
self.capital_used += transaction.amount * transaction.price
|
|
||||||
|
|
||||||
if transaction.amount > 0:
|
|
||||||
if order_position.cost_basis > 0:
|
|
||||||
order_position.cost_basis = np.average(
|
|
||||||
[order_position.cost_basis, transaction.price],
|
|
||||||
weights=[order_position.amount, transaction.amount]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
order_position.cost_basis = transaction.price
|
|
||||||
|
|
||||||
log.debug('updated portfolio with executed order')
|
|
||||||
|
|
||||||
def remove_order(self, order):
|
def remove_order(self, order):
|
||||||
"""
|
"""
|
||||||
Removing an open order.
|
Removing an open order.
|
||||||
@@ -125,7 +116,7 @@ class ExchangePortfolio(Portfolio):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
log.info('removing cancelled order {}'.format(order.id))
|
log.info('removing cancelled order {}'.format(order.id))
|
||||||
del self.open_orders[order.id]
|
self._remove_open_order(order)
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# 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 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
|
||||||
|
from logbook import Logger
|
||||||
|
from numpy import (
|
||||||
|
iinfo,
|
||||||
|
uint32,
|
||||||
|
)
|
||||||
|
|
||||||
|
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]
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
from catalyst.exchange.bitfinex.bitfinex import Bitfinex
|
|
||||||
from catalyst.exchange.bittrex.bittrex import Bittrex
|
|
||||||
from catalyst.exchange.exchange_errors import ExchangeNotFoundError
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_auth
|
|
||||||
from catalyst.exchange.poloniex.poloniex import Poloniex
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchange(exchange_name, base_currency=None):
|
|
||||||
exchange_auth = get_exchange_auth(exchange_name)
|
|
||||||
if exchange_name == 'bitfinex':
|
|
||||||
return Bitfinex(
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'],
|
|
||||||
base_currency=base_currency,
|
|
||||||
portfolio=None
|
|
||||||
)
|
|
||||||
|
|
||||||
elif exchange_name == 'bittrex':
|
|
||||||
return Bittrex(
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'],
|
|
||||||
base_currency=base_currency,
|
|
||||||
portfolio=None
|
|
||||||
)
|
|
||||||
|
|
||||||
elif exchange_name == 'poloniex':
|
|
||||||
return Poloniex(
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'],
|
|
||||||
base_currency=base_currency,
|
|
||||||
portfolio=None
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ExchangeNotFoundError(exchange_name=exchange_name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchanges(exchange_names):
|
|
||||||
exchanges = dict()
|
|
||||||
for exchange_name in exchange_names:
|
|
||||||
exchanges[exchange_name] = get_exchange(exchange_name)
|
|
||||||
|
|
||||||
return exchanges
|
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
from catalyst.exchange.utils.stats_utils import prepare_stats
|
||||||
from catalyst.gens.sim_engine import (
|
from catalyst.gens.sim_engine import (
|
||||||
BAR,
|
BAR,
|
||||||
SESSION_START
|
SESSION_START
|
||||||
)
|
)
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.exchange.exchange_errors import \
|
|
||||||
MismatchingBaseCurrenciesExchanges
|
|
||||||
|
|
||||||
log = Logger('LiveGraphClock', level=LOG_LEVEL)
|
log = Logger('LiveGraphClock', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
@@ -38,177 +36,23 @@ 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, time_skew=pd.Timedelta('0s')):
|
def __init__(self, sessions, context, callback=None,
|
||||||
|
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.fmt = mdates.DateFormatter('%Y-%m-%d %H:%M')
|
self.callback = callback
|
||||||
|
|
||||||
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('1 min')
|
current_minute = current_time.floor('1T')
|
||||||
|
|
||||||
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))
|
||||||
@@ -216,14 +60,11 @@ class LiveGraphClock(object):
|
|||||||
self._last_emit = current_minute
|
self._last_emit = current_minute
|
||||||
yield current_minute, BAR
|
yield current_minute, BAR
|
||||||
|
|
||||||
try:
|
recorded_cols = list(self.context.recorded_vars.keys())
|
||||||
self.draw_pnl()
|
df, _ = prepare_stats(
|
||||||
self.draw_custom_signals()
|
self.context.frame_stats, recorded_cols=recorded_cols
|
||||||
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
|
||||||
|
|||||||
@@ -1,651 +0,0 @@
|
|||||||
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 = {}
|
|
||||||
self.load_assets()
|
|
||||||
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)
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
# TODO: what's wrong with this?
|
|
||||||
# end = int(time.mktime(end_dt.timetuple()))
|
|
||||||
end = int(time.time())
|
|
||||||
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
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
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).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')
|
|
||||||
@@ -14,14 +14,13 @@
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.gens.sim_engine import (
|
from catalyst.gens.sim_engine import (
|
||||||
BAR,
|
BAR,
|
||||||
SESSION_START
|
SESSION_START
|
||||||
)
|
)
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
|
|
||||||
log = Logger('ExchangeClock', level=LOG_LEVEL)
|
log = Logger('ExchangeClock', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
@@ -31,7 +30,8 @@ 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 around the clock.
|
This is a stripped down version because crypto exchanges run
|
||||||
|
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.
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
|
|
||||||
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 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 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 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}{market} cost basis {cost_basis:.4f}{base}'.format(
|
|
||||||
amount=position['amount'],
|
|
||||||
market=position['sid'].market_currency,
|
|
||||||
cost_basis=position['cost_basis'],
|
|
||||||
base=position['sid'].base_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()
|
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst.data.bundles.core import download_without_progress
|
||||||
|
from catalyst.exchange.utils.exchange_utils import get_exchange_bundles_folder
|
||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst.data.bundles.core import download_without_progress
|
||||||
|
from catalyst.exchange.utils.exchange_utils import get_exchange_bundles_folder
|
||||||
|
|
||||||
|
EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex']
|
||||||
|
API_URL = 'http://data.enigma.co/api/v1'
|
||||||
|
|
||||||
|
|
||||||
|
def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
||||||
|
"""
|
||||||
|
Download and extract a bcolz bundle.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
symbol: str
|
||||||
|
data_frequency: str
|
||||||
|
period: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
Filename: bitfinex-daily-neo_eth-2017-10.tar.gz
|
||||||
|
|
||||||
|
"""
|
||||||
|
root = get_exchange_bundles_folder(exchange_name)
|
||||||
|
name = '{exchange}-{frequency}-{symbol}-{period}'.format(
|
||||||
|
exchange=exchange_name,
|
||||||
|
frequency=data_frequency,
|
||||||
|
symbol=symbol,
|
||||||
|
period=period
|
||||||
|
)
|
||||||
|
path = os.path.join(root, name)
|
||||||
|
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
url = 'https://s3.amazonaws.com/enigmaco/catalyst-bundles/' \
|
||||||
|
'exchange-{exchange}/{name}.tar.gz'.format(
|
||||||
|
exchange=exchange_name,
|
||||||
|
name=name)
|
||||||
|
|
||||||
|
bytes = download_without_progress(url)
|
||||||
|
with tarfile.open('r', fileobj=bytes) as tar:
|
||||||
|
tar.extractall(path)
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def get_df_from_arrays(arrays, periods):
|
||||||
|
"""
|
||||||
|
A DataFrame from the specified OHCLV arrays.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
arrays: Object
|
||||||
|
periods: DateTimeIndex
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DataFrame
|
||||||
|
|
||||||
|
"""
|
||||||
|
ohlcv = dict()
|
||||||
|
for index, field in enumerate(
|
||||||
|
['open', 'high', 'low', 'close', 'volume']):
|
||||||
|
ohlcv[field] = arrays[index].flatten()
|
||||||
|
|
||||||
|
df = pd.DataFrame(
|
||||||
|
data=ohlcv,
|
||||||
|
index=periods
|
||||||
|
)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def range_in_bundle(asset, start_dt, end_dt, reader):
|
||||||
|
"""
|
||||||
|
Evaluate whether price data of an asset is included has been ingested in
|
||||||
|
the exchange bundle for the given date range.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asset: TradingPair
|
||||||
|
start_dt: datetime
|
||||||
|
end_dt: datetime
|
||||||
|
reader: BcolzBarMinuteReader
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
has_data = True
|
||||||
|
dates = [start_dt, end_dt]
|
||||||
|
|
||||||
|
while dates and has_data:
|
||||||
|
try:
|
||||||
|
dt = dates.pop(0)
|
||||||
|
close = reader.get_value(asset.sid, dt, 'close')
|
||||||
|
|
||||||
|
if np.isnan(close):
|
||||||
|
has_data = False
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
has_data = False
|
||||||
|
|
||||||
|
return has_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_assets(exchange, include_symbols, exclude_symbols):
|
||||||
|
"""
|
||||||
|
Get assets from an exchange, including or excluding the specified
|
||||||
|
symbols.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange: Exchange
|
||||||
|
include_symbols: str
|
||||||
|
exclude_symbols: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[TradingPair]
|
||||||
|
|
||||||
|
"""
|
||||||
|
if include_symbols is not None:
|
||||||
|
include_symbols_list = include_symbols.split(',')
|
||||||
|
|
||||||
|
return exchange.get_assets(include_symbols_list)
|
||||||
|
|
||||||
|
else:
|
||||||
|
all_assets = exchange.get_assets()
|
||||||
|
|
||||||
|
if exclude_symbols is not None:
|
||||||
|
exclude_symbols_list = exclude_symbols.split(',')
|
||||||
|
|
||||||
|
assets = []
|
||||||
|
for asset in all_assets:
|
||||||
|
if asset.symbol not in exclude_symbols_list:
|
||||||
|
assets.append(asset)
|
||||||
|
|
||||||
|
return assets
|
||||||
|
|
||||||
|
else:
|
||||||
|
return all_assets
|
||||||
@@ -1,17 +1,12 @@
|
|||||||
import calendar
|
import calendar
|
||||||
import os
|
import re
|
||||||
import tarfile
|
from datetime import datetime, timedelta, date
|
||||||
from datetime import timedelta, datetime, date
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from catalyst.data.bundles.core import download_without_progress
|
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_bundles_folder
|
InvalidHistoryFrequencyAlias
|
||||||
|
|
||||||
EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex']
|
|
||||||
API_URL = 'http://data.enigma.co/api/v1'
|
|
||||||
|
|
||||||
|
|
||||||
def get_date_from_ms(ms):
|
def get_date_from_ms(ms):
|
||||||
@@ -49,46 +44,6 @@ def get_seconds_from_date(date):
|
|||||||
return int((date - epoch).total_seconds())
|
return int((date - epoch).total_seconds())
|
||||||
|
|
||||||
|
|
||||||
def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
|
||||||
"""
|
|
||||||
Download and extract a bcolz bundle.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
exchange_name: str
|
|
||||||
symbol: str
|
|
||||||
data_frequency: str
|
|
||||||
period: str
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
Filename: bitfinex-daily-neo_eth-2017-10.tar.gz
|
|
||||||
|
|
||||||
"""
|
|
||||||
root = get_exchange_bundles_folder(exchange_name)
|
|
||||||
name = '{exchange}-{frequency}-{symbol}-{period}'.format(
|
|
||||||
exchange=exchange_name,
|
|
||||||
frequency=data_frequency,
|
|
||||||
symbol=symbol,
|
|
||||||
period=period
|
|
||||||
)
|
|
||||||
path = os.path.join(root, name)
|
|
||||||
|
|
||||||
if not os.path.isdir(path):
|
|
||||||
url = 'https://s3.amazonaws.com/enigmaco/catalyst-bundles/' \
|
|
||||||
'exchange-{exchange}/{name}.tar.gz'.format(
|
|
||||||
exchange=exchange_name,
|
|
||||||
name=name
|
|
||||||
)
|
|
||||||
|
|
||||||
bytes = download_without_progress(url)
|
|
||||||
with tarfile.open('r', fileobj=bytes) as tar:
|
|
||||||
tar.extractall(path)
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def get_delta(periods, data_frequency):
|
def get_delta(periods, data_frequency):
|
||||||
"""
|
"""
|
||||||
Get a time delta based on the specified data frequency.
|
Get a time delta based on the specified data frequency.
|
||||||
@@ -107,7 +62,7 @@ def get_delta(periods, data_frequency):
|
|||||||
if data_frequency == 'minute' else timedelta(days=periods)
|
if data_frequency == 'minute' else timedelta(days=periods)
|
||||||
|
|
||||||
|
|
||||||
def get_periods_range(start_dt, end_dt, freq):
|
def get_periods_range(freq, start_dt=None, end_dt=None, periods=None):
|
||||||
"""
|
"""
|
||||||
Get a date range for the specified parameters.
|
Get a date range for the specified parameters.
|
||||||
|
|
||||||
@@ -128,7 +83,38 @@ def get_periods_range(start_dt, end_dt, freq):
|
|||||||
elif freq == 'daily':
|
elif freq == 'daily':
|
||||||
freq = 'D'
|
freq = 'D'
|
||||||
|
|
||||||
return pd.date_range(start_dt, end_dt, freq=freq)
|
if start_dt is not None and end_dt is not None and periods is None:
|
||||||
|
|
||||||
|
return pd.date_range(start_dt, end_dt, freq=freq)
|
||||||
|
|
||||||
|
elif periods is not None and (start_dt is not None or end_dt is not None):
|
||||||
|
_, unit_periods, unit, _ = get_frequency(freq)
|
||||||
|
adj_periods = periods * unit_periods
|
||||||
|
|
||||||
|
# TODO: standardize time aliases to avoid any mapping
|
||||||
|
unit = 'd' if unit == 'D' else 'm'
|
||||||
|
delta = pd.Timedelta(adj_periods, unit)
|
||||||
|
|
||||||
|
if start_dt is not None:
|
||||||
|
return pd.date_range(
|
||||||
|
start=start_dt,
|
||||||
|
end=start_dt + delta,
|
||||||
|
freq=freq,
|
||||||
|
closed='left',
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return pd.date_range(
|
||||||
|
start=end_dt - delta,
|
||||||
|
end=end_dt,
|
||||||
|
freq=freq,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'Choose only two parameters between start_dt, end_dt '
|
||||||
|
'and periods.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_periods(start_dt, end_dt, freq):
|
def get_periods(start_dt, end_dt, freq):
|
||||||
@@ -146,10 +132,10 @@ def get_periods(start_dt, end_dt, freq):
|
|||||||
int
|
int
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return len(get_periods_range(start_dt, end_dt, freq))
|
return len(get_periods_range(start_dt=start_dt, end_dt=end_dt, freq=freq))
|
||||||
|
|
||||||
|
|
||||||
def get_start_dt(end_dt, bar_count, data_frequency):
|
def get_start_dt(end_dt, bar_count, data_frequency, include_first=True):
|
||||||
"""
|
"""
|
||||||
The start date based on specified end date and data frequency.
|
The start date based on specified end date and data frequency.
|
||||||
|
|
||||||
@@ -158,6 +144,7 @@ def get_start_dt(end_dt, bar_count, data_frequency):
|
|||||||
end_dt: datetime
|
end_dt: datetime
|
||||||
bar_count: int
|
bar_count: int
|
||||||
data_frequency: str
|
data_frequency: str
|
||||||
|
include_first
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -168,6 +155,9 @@ def get_start_dt(end_dt, bar_count, data_frequency):
|
|||||||
if periods > 1:
|
if periods > 1:
|
||||||
delta = get_delta(periods, data_frequency)
|
delta = get_delta(periods, data_frequency)
|
||||||
start_dt = end_dt - delta
|
start_dt = end_dt - delta
|
||||||
|
|
||||||
|
if not include_first:
|
||||||
|
start_dt += get_delta(1, data_frequency)
|
||||||
else:
|
else:
|
||||||
start_dt = end_dt
|
start_dt = end_dt
|
||||||
|
|
||||||
@@ -188,8 +178,10 @@ def get_period_label(dt, data_frequency):
|
|||||||
str
|
str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return '{}-{:02d}'.format(dt.year, dt.month) if data_frequency == 'minute' \
|
if data_frequency == 'minute':
|
||||||
else '{}'.format(dt.year)
|
return '{}-{:02d}'.format(dt.year, dt.month)
|
||||||
|
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):
|
||||||
@@ -256,61 +248,80 @@ def get_year_start_end(dt, first_day=None, last_day=None):
|
|||||||
return year_start, year_end
|
return year_start, year_end
|
||||||
|
|
||||||
|
|
||||||
def get_df_from_arrays(arrays, periods):
|
def get_frequency(freq, data_frequency=None):
|
||||||
"""
|
"""
|
||||||
A DataFrame from the specified OHCLV arrays.
|
Get the frequency parameters.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
We're trying to use Pandas convention for frequency aliases.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
arrays: Object
|
freq: str
|
||||||
periods: DateTimeIndex
|
data_frequency: str
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
DataFrame
|
str, int, str, str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ohlcv = dict()
|
if data_frequency is None:
|
||||||
for index, field in enumerate(
|
data_frequency = 'daily' if freq.upper().endswith('D') else 'minute'
|
||||||
['open', 'high', 'low', 'close', 'volume']):
|
|
||||||
ohlcv[field] = arrays[index].flatten()
|
|
||||||
|
|
||||||
df = pd.DataFrame(
|
if freq == 'minute':
|
||||||
data=ohlcv,
|
unit = 'T'
|
||||||
index=periods
|
candle_size = 1
|
||||||
)
|
|
||||||
return df
|
elif freq == 'daily':
|
||||||
|
unit = 'D'
|
||||||
|
candle_size = 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
freq_match = re.match(r'([0-9].*)?(m|M|d|D|h|H|T)', freq, re.M | re.I)
|
||||||
|
if freq_match:
|
||||||
|
candle_size = int(freq_match.group(1)) if freq_match.group(1) \
|
||||||
|
else 1
|
||||||
|
unit = freq_match.group(2)
|
||||||
|
|
||||||
|
else:
|
||||||
|
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':
|
||||||
|
unit = 'D'
|
||||||
|
alias = '{}D'.format(candle_size)
|
||||||
|
|
||||||
|
if data_frequency == 'minute':
|
||||||
|
data_frequency = 'daily'
|
||||||
|
|
||||||
|
elif unit.lower() == 'm' or unit == 'T':
|
||||||
|
unit = 'T'
|
||||||
|
alias = '{}T'.format(candle_size)
|
||||||
|
|
||||||
|
if data_frequency == 'daily':
|
||||||
|
data_frequency = 'minute'
|
||||||
|
|
||||||
|
# elif unit.lower() == 'h':
|
||||||
|
# candle_size = candle_size * 60
|
||||||
|
#
|
||||||
|
# alias = '{}T'.format(candle_size)
|
||||||
|
# if data_frequency == 'daily':
|
||||||
|
# data_frequency = 'minute'
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise InvalidHistoryFrequencyAlias(freq=freq)
|
||||||
|
|
||||||
|
return alias, candle_size, unit, data_frequency
|
||||||
|
|
||||||
|
|
||||||
def range_in_bundle(asset, start_dt, end_dt, reader):
|
def from_ms_timestamp(ms):
|
||||||
"""
|
return pd.to_datetime(ms, unit='ms', utc=True)
|
||||||
Evaluate whether price data of an asset is included has been ingested in
|
|
||||||
the exchange bundle for the given date range.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
asset: TradingPair
|
|
||||||
start_dt: datetime
|
|
||||||
end_dt: datetime
|
|
||||||
reader: BcolzBarMinuteReader
|
|
||||||
|
|
||||||
Returns
|
def get_epoch():
|
||||||
-------
|
return pd.to_datetime('1970-1-1', utc=True)
|
||||||
bool
|
|
||||||
|
|
||||||
"""
|
|
||||||
has_data = True
|
|
||||||
dates = [start_dt, end_dt]
|
|
||||||
|
|
||||||
while dates and has_data:
|
|
||||||
try:
|
|
||||||
dt = dates.pop(0)
|
|
||||||
close = reader.get_value(asset.sid, dt, 'close')
|
|
||||||
|
|
||||||
if np.isnan(close):
|
|
||||||
has_data = False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
has_data = False
|
|
||||||
|
|
||||||
return has_data
|
|
||||||
@@ -1,20 +1,41 @@
|
|||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import shutil
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from catalyst.assets._assets import TradingPair
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from six import string_types
|
||||||
from six.moves.urllib import request
|
from six.moves.urllib import request
|
||||||
|
|
||||||
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound, \
|
from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
|
||||||
InvalidHistoryFrequencyError, InvalidHistoryFrequencyAlias
|
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound
|
||||||
|
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
|
||||||
|
|
||||||
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
|
|
||||||
'{exchange}/symbols.json'
|
def get_sid(symbol):
|
||||||
|
"""
|
||||||
|
Create a sid by hashing the symbol of a currency pair.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
symbol: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
The resulting sid.
|
||||||
|
|
||||||
|
"""
|
||||||
|
sid = int(
|
||||||
|
hashlib.sha256(symbol.encode('utf-8')).hexdigest(), 16
|
||||||
|
) % 10 ** 6
|
||||||
|
return sid
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_folder(exchange_name, environ=None):
|
def get_exchange_folder(exchange_name, environ=None):
|
||||||
@@ -41,7 +62,14 @@ def get_exchange_folder(exchange_name, environ=None):
|
|||||||
return exchange_folder
|
return exchange_folder
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_symbols_filename(exchange_name, environ=None):
|
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):
|
||||||
"""
|
"""
|
||||||
The absolute path of the exchange's symbol.json file.
|
The absolute path of the exchange's symbol.json file.
|
||||||
|
|
||||||
@@ -55,8 +83,9 @@ def get_exchange_symbols_filename(exchange_name, environ=None):
|
|||||||
str
|
str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
name = 'symbols.json' if not is_local else 'symbols_local.json'
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
return os.path.join(exchange_folder, 'symbols.json')
|
return os.path.join(exchange_folder, name)
|
||||||
|
|
||||||
|
|
||||||
def download_exchange_symbols(exchange_name, environ=None):
|
def download_exchange_symbols(exchange_name, environ=None):
|
||||||
@@ -79,13 +108,14 @@ def download_exchange_symbols(exchange_name, environ=None):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_symbols(exchange_name, environ=None):
|
def get_exchange_symbols(exchange_name, is_local=False, environ=None):
|
||||||
"""
|
"""
|
||||||
The de-serialized content of the exchange's symbols.json.
|
The de-serialized content of the exchange's symbols.json.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
exchange_name: str
|
exchange_name: str
|
||||||
|
is_local: bool
|
||||||
environ:
|
environ:
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
@@ -93,18 +123,24 @@ def get_exchange_symbols(exchange_name, environ=None):
|
|||||||
Object
|
Object
|
||||||
|
|
||||||
"""
|
"""
|
||||||
filename = get_exchange_symbols_filename(exchange_name)
|
filename = get_exchange_symbols_filename(exchange_name, is_local)
|
||||||
|
|
||||||
if not os.path.isfile(filename) or \
|
if not is_local and (not os.path.isfile(filename) or pd.Timedelta(
|
||||||
pd.Timedelta(pd.Timestamp('now',
|
pd.Timestamp('now', tz='UTC') - last_modified_time(
|
||||||
tz='UTC') - last_modified_time(
|
filename)).days > 1):
|
||||||
filename)).days > 1:
|
try:
|
||||||
download_exchange_symbols(exchange_name, environ)
|
download_exchange_symbols(exchange_name, environ)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
with open(filename) as data_file:
|
with open(filename) as data_file:
|
||||||
data = json.load(data_file)
|
try:
|
||||||
return data
|
data = json.load(data_file, cls=ExchangeJSONDecoder)
|
||||||
|
return data
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
return dict()
|
||||||
else:
|
else:
|
||||||
raise ExchangeSymbolsNotFound(
|
raise ExchangeSymbolsNotFound(
|
||||||
exchange=exchange_name,
|
exchange=exchange_name,
|
||||||
@@ -112,6 +148,32 @@ def get_exchange_symbols(exchange_name, environ=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_exchange_symbols(exchange_name, assets, is_local=False, environ=None):
|
||||||
|
"""
|
||||||
|
Save assets into an exchange_symbols file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
assets: list[dict[str, object]]
|
||||||
|
is_local: bool
|
||||||
|
environ
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
asset_dicts = dict()
|
||||||
|
for symbol in assets:
|
||||||
|
asset_dicts[symbol] = assets[symbol].to_dict()
|
||||||
|
|
||||||
|
filename = get_exchange_symbols_filename(
|
||||||
|
exchange_name, is_local, environ
|
||||||
|
)
|
||||||
|
with open(filename, 'wt') as handle:
|
||||||
|
json.dump(asset_dicts, handle, indent=4, default=symbols_serial)
|
||||||
|
|
||||||
|
|
||||||
def get_symbols_string(assets):
|
def get_symbols_string(assets):
|
||||||
"""
|
"""
|
||||||
A concatenated string of symbols from a list of assets.
|
A concatenated string of symbols from a list of assets.
|
||||||
@@ -129,7 +191,7 @@ def get_symbols_string(assets):
|
|||||||
return ', '.join([asset.symbol for asset in array])
|
return ', '.join([asset.symbol for asset in array])
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_auth(exchange_name, environ=None):
|
def get_exchange_auth(exchange_name, alias=None, environ=None):
|
||||||
"""
|
"""
|
||||||
The de-serialized contend of the exchange's auth.json file.
|
The de-serialized contend of the exchange's auth.json file.
|
||||||
|
|
||||||
@@ -144,7 +206,8 @@ def get_exchange_auth(exchange_name, environ=None):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
filename = os.path.join(exchange_folder, 'auth.json')
|
name = 'auth' if alias is None else alias
|
||||||
|
filename = os.path.join(exchange_folder, '{}.json'.format(name))
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
with open(filename) as data_file:
|
with open(filename) as data_file:
|
||||||
@@ -158,6 +221,24 @@ def get_exchange_auth(exchange_name, environ=None):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def delete_algo_folder(algo_name, environ=None):
|
||||||
|
"""
|
||||||
|
Delete the folder containing the algo state.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
algo_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
folder = get_algo_folder(algo_name, environ)
|
||||||
|
shutil.rmtree(folder)
|
||||||
|
|
||||||
|
|
||||||
def get_algo_folder(algo_name, environ=None):
|
def get_algo_folder(algo_name, environ=None):
|
||||||
"""
|
"""
|
||||||
The algorithm root folder of the algorithm.
|
The algorithm root folder of the algorithm.
|
||||||
@@ -182,7 +263,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):
|
def get_algo_object(algo_name, key, environ=None, rel_path=None, how='pickle'):
|
||||||
"""
|
"""
|
||||||
The de-serialized object of the algo name and key.
|
The de-serialized object of the algo name and key.
|
||||||
|
|
||||||
@@ -206,19 +287,25 @@ def get_algo_object(algo_name, key, environ=None, rel_path=None):
|
|||||||
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)
|
||||||
|
|
||||||
filename = os.path.join(folder, key + '.p')
|
name = '{}.p'.format(key) if how == 'pickle' else '{}.json'.format(key)
|
||||||
|
filename = os.path.join(folder, name)
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
try:
|
if how == 'pickle':
|
||||||
with open(filename, 'rb') as handle:
|
with open(filename, 'rb') as handle:
|
||||||
return pickle.load(handle)
|
return pickle.load(handle)
|
||||||
except Exception as e:
|
|
||||||
return None
|
else:
|
||||||
|
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.
|
||||||
|
|
||||||
@@ -237,10 +324,15 @@ 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)
|
||||||
|
|
||||||
filename = os.path.join(folder, key + '.p')
|
if how == 'json':
|
||||||
|
filename = os.path.join(folder, '{}.json'.format(key))
|
||||||
|
with open(filename, 'wt') as handle:
|
||||||
|
json.dump(obj, handle, indent=4, cls=ExchangeJSONEncoder)
|
||||||
|
|
||||||
with open(filename, 'wb') as handle:
|
else:
|
||||||
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
filename = os.path.join(folder, '{}.p'.format(key))
|
||||||
|
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):
|
||||||
@@ -344,6 +436,34 @@ 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):
|
||||||
|
"""
|
||||||
|
JSON serializer for objects not serializable by default json code
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
obj: Object
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(obj, (datetime, date)):
|
||||||
|
return obj.floor('1D').strftime(DATE_FORMAT)
|
||||||
|
|
||||||
|
raise TypeError("Type %s not serializable" % type(obj))
|
||||||
|
|
||||||
|
|
||||||
def perf_serial(obj):
|
def perf_serial(obj):
|
||||||
"""
|
"""
|
||||||
JSON serializer for objects not serializable by default json code
|
JSON serializer for objects not serializable by default json code
|
||||||
@@ -392,67 +512,6 @@ def get_common_assets(exchanges):
|
|||||||
return assets
|
return assets
|
||||||
|
|
||||||
|
|
||||||
def get_frequency(freq, data_frequency):
|
|
||||||
"""
|
|
||||||
Get the frequency parameters.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
We're trying to use Pandas convention for frequency aliases.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
freq: str
|
|
||||||
data_frequency: str
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str, int, str, str
|
|
||||||
|
|
||||||
"""
|
|
||||||
if freq == 'minute':
|
|
||||||
unit = 'T'
|
|
||||||
candle_size = 1
|
|
||||||
|
|
||||||
elif freq == 'daily':
|
|
||||||
unit = 'D'
|
|
||||||
candle_size = 1
|
|
||||||
|
|
||||||
else:
|
|
||||||
freq_match = re.match(r'([0-9].*)?(m|M|d|D|h|H|T)', freq, re.M | re.I)
|
|
||||||
if freq_match:
|
|
||||||
candle_size = int(freq_match.group(1)) if freq_match.group(1) \
|
|
||||||
else 1
|
|
||||||
unit = freq_match.group(2)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise InvalidHistoryFrequencyError(frequency=freq)
|
|
||||||
|
|
||||||
if unit.lower() == 'd':
|
|
||||||
alias = '{}D'.format(candle_size)
|
|
||||||
|
|
||||||
if data_frequency == 'minute':
|
|
||||||
data_frequency = 'daily'
|
|
||||||
|
|
||||||
elif unit.lower() == 'm' or unit == 'T':
|
|
||||||
alias = '{}T'.format(candle_size)
|
|
||||||
|
|
||||||
if data_frequency == 'daily':
|
|
||||||
data_frequency = 'minute'
|
|
||||||
|
|
||||||
# elif unit.lower() == 'h':
|
|
||||||
# candle_size = candle_size * 60
|
|
||||||
#
|
|
||||||
# alias = '{}T'.format(candle_size)
|
|
||||||
# if data_frequency == 'daily':
|
|
||||||
# data_frequency = 'minute'
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise InvalidHistoryFrequencyAlias(freq=freq)
|
|
||||||
|
|
||||||
return alias, candle_size, unit, data_frequency
|
|
||||||
|
|
||||||
|
|
||||||
def resample_history_df(df, freq, field):
|
def resample_history_df(df, freq, field):
|
||||||
"""
|
"""
|
||||||
Resample the OHCLV DataFrame using the specified frequency.
|
Resample the OHCLV DataFrame using the specified frequency.
|
||||||
@@ -481,4 +540,124 @@ def resample_history_df(df, freq, field):
|
|||||||
else:
|
else:
|
||||||
raise ValueError('Invalid field.')
|
raise ValueError('Invalid field.')
|
||||||
|
|
||||||
return df.resample(freq).agg(agg)
|
resampled_df = df.resample(freq).agg(agg)
|
||||||
|
return resampled_df
|
||||||
|
|
||||||
|
|
||||||
|
def mixin_market_params(exchange_name, params, market):
|
||||||
|
"""
|
||||||
|
Applies a CCXT market dict to parameters of TradingPair init.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
params: dict[Object]
|
||||||
|
market: dict[Object]
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: make this more externalized / configurable
|
||||||
|
if 'lot' in market:
|
||||||
|
params['min_trade_size'] = market['lot']
|
||||||
|
params['lot'] = market['lot']
|
||||||
|
|
||||||
|
if exchange_name == 'bitfinex':
|
||||||
|
params['maker'] = 0.001
|
||||||
|
params['taker'] = 0.002
|
||||||
|
|
||||||
|
elif 'maker' in market and 'taker' in market \
|
||||||
|
and market['maker'] is not None and market['taker'] is not None:
|
||||||
|
params['maker'] = market['maker']
|
||||||
|
params['taker'] = market['taker']
|
||||||
|
|
||||||
|
else:
|
||||||
|
# TODO: default commission, make configurable
|
||||||
|
params['maker'] = 0.0015
|
||||||
|
params['taker'] = 0.0025
|
||||||
|
|
||||||
|
info = market['info'] if 'info' in market else None
|
||||||
|
if info:
|
||||||
|
if 'minimum_order_size' in info:
|
||||||
|
params['min_trade_size'] = float(info['minimum_order_size'])
|
||||||
|
|
||||||
|
if 'lot' not in params:
|
||||||
|
params['lot'] = params['min_trade_size']
|
||||||
|
|
||||||
|
|
||||||
|
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,97 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
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
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
log = Logger('factory', level=LOG_LEVEL)
|
||||||
|
exchange_cache = dict()
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange(exchange_name, base_currency=None, must_authenticate=False,
|
||||||
|
skip_init=False, auth_alias=None):
|
||||||
|
key = (exchange_name, base_currency)
|
||||||
|
if key in exchange_cache:
|
||||||
|
return exchange_cache[key]
|
||||||
|
|
||||||
|
exchange_auth = get_exchange_auth(exchange_name, alias=auth_alias)
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
from json import JSONEncoder
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from catalyst.constants import DATE_TIME_FORMAT
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
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
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
|
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 set_print_settings():
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pretty_stats(stats, recorded_cols=None, num_rows=10, show_tail=True):
|
||||||
|
"""
|
||||||
|
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 = list(stats.T.to_dict().values())
|
||||||
|
stats.sort(key=itemgetter('period_close'))
|
||||||
|
|
||||||
|
if len(stats) > num_rows:
|
||||||
|
display_stats = stats[-num_rows:] if show_tail else stats[0:num_rows]
|
||||||
|
else:
|
||||||
|
display_stats = stats
|
||||||
|
|
||||||
|
df, columns = prepare_stats(
|
||||||
|
display_stats, recorded_cols=recorded_cols
|
||||||
|
)
|
||||||
|
set_print_settings()
|
||||||
|
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('//')
|
||||||
|
path = '{folder}/{algo}/{time}-{algo}-{pid}.csv'.format(
|
||||||
|
folder=folder,
|
||||||
|
algo=algo_namespace,
|
||||||
|
time=timestr,
|
||||||
|
pid=pid,
|
||||||
|
)
|
||||||
|
obj = s3.Object(parts[1], path)
|
||||||
|
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_orders(perf):
|
||||||
|
order_list = perf.orders.values
|
||||||
|
all_orders = [t for sublist in order_list for t in sublist]
|
||||||
|
all_orders.sort(key=lambda o: o['dt'])
|
||||||
|
|
||||||
|
orders = pd.DataFrame(all_orders)
|
||||||
|
if not orders.empty:
|
||||||
|
orders.set_index('dt', inplace=True, drop=True)
|
||||||
|
return orders
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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):
|
||||||
|
asset_folder = '{}_{}'.format(assets.exchange, assets.symbol)
|
||||||
|
else:
|
||||||
|
asset_folder = ','.join(
|
||||||
|
['{}_{}'.format(a.exchange, a.symbol) for a in assets]
|
||||||
|
)
|
||||||
|
|
||||||
|
folder = os.path.join(
|
||||||
|
tempfile.gettempdir(), 'catalyst', 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, folder
|
||||||
@@ -15,13 +15,8 @@
|
|||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
|
||||||
from sys import float_info
|
|
||||||
|
|
||||||
from six import with_metaclass
|
|
||||||
|
|
||||||
import catalyst.utils.math_utils as zp_math
|
|
||||||
|
|
||||||
from numpy import isfinite
|
from numpy import isfinite
|
||||||
|
from six import with_metaclass
|
||||||
|
|
||||||
from catalyst.errors import BadOrderParameters
|
from catalyst.errors import BadOrderParameters
|
||||||
|
|
||||||
@@ -77,6 +72,7 @@ class LimitOrder(ExecutionStyle):
|
|||||||
Execution style representing an order to be executed at a price equal to or
|
Execution style representing an order to be executed at a price equal to or
|
||||||
better than a specified limit price.
|
better than a specified limit price.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, limit_price, exchange=None):
|
def __init__(self, limit_price, exchange=None):
|
||||||
"""
|
"""
|
||||||
Store the given price.
|
Store the given price.
|
||||||
@@ -99,6 +95,7 @@ class StopOrder(ExecutionStyle):
|
|||||||
Execution style representing an order to be placed once the market price
|
Execution style representing an order to be placed once the market price
|
||||||
reaches a specified stop price.
|
reaches a specified stop price.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, stop_price, exchange=None):
|
def __init__(self, stop_price, exchange=None):
|
||||||
"""
|
"""
|
||||||
Store the given price.
|
Store the given price.
|
||||||
@@ -121,6 +118,7 @@ class StopLimitOrder(ExecutionStyle):
|
|||||||
Execution style representing a limit order to be placed with a specified
|
Execution style representing a limit order to be placed with a specified
|
||||||
limit price once the market reaches a specified stop price.
|
limit price once the market reaches a specified stop price.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, limit_price, stop_price, exchange=None):
|
def __init__(self, limit_price, stop_price, exchange=None):
|
||||||
"""
|
"""
|
||||||
Store the given prices
|
Store the given prices
|
||||||
@@ -144,31 +142,20 @@ class StopLimitOrder(ExecutionStyle):
|
|||||||
def asymmetric_round_price_to_penny(price, prefer_round_down,
|
def asymmetric_round_price_to_penny(price, prefer_round_down,
|
||||||
diff=(0.0095 - .005)):
|
diff=(0.0095 - .005)):
|
||||||
"""
|
"""
|
||||||
Asymmetric rounding function for adjusting prices to two places in a way
|
Modified the original function because we do not want to round
|
||||||
that "improves" the price. For limit prices, this means preferring to
|
prices on crypto exchange.
|
||||||
round down on buys and preferring to round up on sells. For stop prices,
|
|
||||||
it means the reverse.
|
|
||||||
|
|
||||||
If prefer_round_down == True:
|
Parameters
|
||||||
When .05 below to .95 above a penny, use that penny.
|
----------
|
||||||
If prefer_round_down == False:
|
price: float
|
||||||
When .95 below to .05 above a penny, use that penny.
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
In math-speak:
|
|
||||||
If prefer_round_down: [<X-1>.0095, X.0195) -> round to X.01.
|
|
||||||
If not prefer_round_down: (<X-1>.0005, X.0105] -> round to X.01.
|
|
||||||
"""
|
"""
|
||||||
# Subtracting an epsilon from diff to enforce the open-ness of the upper
|
# TODO: consider overriding outside of the original function
|
||||||
# bound on buys and the lower bound on sells. Using the actual system
|
return price
|
||||||
# epsilon doesn't quite get there, so use a slightly less epsilon-ey value.
|
|
||||||
epsilon = float_info.epsilon * 10
|
|
||||||
diff = diff - epsilon
|
|
||||||
|
|
||||||
# relies on rounding half away from zero, unlike numpy's bankers' rounding
|
|
||||||
rounded = round(price - (diff if prefer_round_down else -diff), 2)
|
|
||||||
if zp_math.tolerant_equals(rounded, 0.0):
|
|
||||||
return 0.0
|
|
||||||
return rounded
|
|
||||||
|
|
||||||
|
|
||||||
def check_stoplimit_prices(price, label):
|
def check_stoplimit_prices(price, label):
|
||||||
|
|||||||
@@ -22,27 +22,26 @@ from pandas.tseries.tools import normalize_date
|
|||||||
|
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
|
|
||||||
from . risk import (
|
from .risk import (
|
||||||
check_entry,
|
check_entry,
|
||||||
choose_treasury
|
choose_treasury
|
||||||
)
|
)
|
||||||
|
|
||||||
from empyrical import (
|
from catalyst.patches.stats 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
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
log = logbook.Logger('Risk Cumulative', level=LOG_LEVEL)
|
log = logbook.Logger('Risk Cumulative', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
choose_treasury = functools.partial(choose_treasury, lambda *args: '10year',
|
choose_treasury = functools.partial(choose_treasury, lambda *args: '10year',
|
||||||
compound=False)
|
compound=False)
|
||||||
|
|
||||||
@@ -145,6 +144,8 @@ class RiskMetricsCumulative(object):
|
|||||||
self.num_trading_days = 0
|
self.num_trading_days = 0
|
||||||
|
|
||||||
def update(self, dt, algorithm_returns, benchmark_returns, leverage):
|
def update(self, dt, algorithm_returns, benchmark_returns, leverage):
|
||||||
|
warnings.filterwarnings('error')
|
||||||
|
|
||||||
# Keep track of latest dt for use in to_dict and other methods
|
# Keep track of latest dt for use in to_dict and other methods
|
||||||
# that report current state.
|
# that report current state.
|
||||||
self.latest_dt = dt
|
self.latest_dt = dt
|
||||||
@@ -160,9 +161,13 @@ 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)
|
||||||
|
|
||||||
self.algorithm_cumulative_returns[dt_loc] = cum_returns(
|
try:
|
||||||
self.algorithm_returns
|
self.algorithm_cumulative_returns[dt_loc] = cum_returns(
|
||||||
)[-1]
|
self.algorithm_returns
|
||||||
|
)[-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]
|
||||||
@@ -191,9 +196,15 @@ class RiskMetricsCumulative(object):
|
|||||||
if len(self.benchmark_returns) == 1:
|
if len(self.benchmark_returns) == 1:
|
||||||
self.benchmark_returns = np.append(0.0, self.benchmark_returns)
|
self.benchmark_returns = np.append(0.0, self.benchmark_returns)
|
||||||
|
|
||||||
self.benchmark_cumulative_returns[dt_loc] = cum_returns(
|
try:
|
||||||
self.benchmark_returns
|
self.benchmark_cumulative_returns[dt_loc] = cum_returns(
|
||||||
)[-1]
|
self.benchmark_returns
|
||||||
|
)[-1]
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(
|
||||||
|
'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]
|
||||||
@@ -265,24 +276,49 @@ 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(
|
|
||||||
self.algorithm_returns
|
try:
|
||||||
)
|
self.downside_risk[dt_loc] = downside_risk(
|
||||||
self.sortino[dt_loc] = sortino_ratio(
|
self.algorithm_returns
|
||||||
self.algorithm_returns,
|
)
|
||||||
_downside_risk=self.downside_risk[dt_loc]
|
except Exception as e:
|
||||||
)
|
log.debug(
|
||||||
|
'unable to calculate downside risk returns: {}'.format(e)
|
||||||
|
)
|
||||||
|
self.downside_risk[dt_loc] = np.nan
|
||||||
|
|
||||||
|
try:
|
||||||
|
risk = self.downside_risk[dt_loc]
|
||||||
|
self.sortino[dt_loc] = sortino_ratio(
|
||||||
|
self.algorithm_returns,
|
||||||
|
_downside_risk=risk
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(
|
||||||
|
'unable to calculate benchmark cum returns: {}'.format(e)
|
||||||
|
)
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
self.max_drawdown = max_drawdown(
|
try:
|
||||||
self.algorithm_returns
|
self.max_drawdown = max_drawdown(
|
||||||
)
|
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
|
||||||
|
|
||||||
|
warnings.resetwarnings()
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
Creates a dictionary representing the state of the risk report.
|
Creates a dictionary representing the state of the risk report.
|
||||||
@@ -294,18 +330,18 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
|
|||||||
rval = {
|
rval = {
|
||||||
'trading_days': self.num_trading_days,
|
'trading_days': self.num_trading_days,
|
||||||
'benchmark_volatility':
|
'benchmark_volatility':
|
||||||
self.benchmark_volatility[dt_loc],
|
self.benchmark_volatility[dt_loc],
|
||||||
'algo_volatility':
|
'algo_volatility':
|
||||||
self.algorithm_volatility[dt_loc],
|
self.algorithm_volatility[dt_loc],
|
||||||
'treasury_period_return': self.treasury_period_return,
|
'treasury_period_return': self.treasury_period_return,
|
||||||
# Though the two following keys say period return,
|
# Though the two following keys say period return,
|
||||||
# they would be more accurately called the cumulative return.
|
# they would be more accurately called the cumulative return.
|
||||||
# However, the keys need to stay the same, for now, for backwards
|
# However, the keys need to stay the same, for now, for backwards
|
||||||
# compatibility with existing consumers.
|
# compatibility with existing consumers.
|
||||||
'algorithm_period_return':
|
'algorithm_period_return':
|
||||||
self.algorithm_cumulative_returns[dt_loc],
|
self.algorithm_cumulative_returns[dt_loc],
|
||||||
'benchmark_period_return':
|
'benchmark_period_return':
|
||||||
self.benchmark_cumulative_returns[dt_loc],
|
self.benchmark_cumulative_returns[dt_loc],
|
||||||
'beta': self.beta[dt_loc],
|
'beta': self.beta[dt_loc],
|
||||||
'alpha': self.alpha[dt_loc],
|
'alpha': self.alpha[dt_loc],
|
||||||
'sharpe': self.sharpe[dt_loc],
|
'sharpe': self.sharpe[dt_loc],
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import warnings
|
||||||
|
|
||||||
import logbook
|
import logbook
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from . import risk
|
from . import risk
|
||||||
from . risk import check_entry
|
from .risk import check_entry
|
||||||
|
|
||||||
from empyrical import (
|
from empyrical import (
|
||||||
alpha_beta_aligned,
|
alpha_beta_aligned,
|
||||||
@@ -78,14 +79,20 @@ class RiskMetricsPeriod(object):
|
|||||||
self.calculate_metrics()
|
self.calculate_metrics()
|
||||||
|
|
||||||
def calculate_metrics(self):
|
def calculate_metrics(self):
|
||||||
self.benchmark_period_returns = \
|
warnings.filterwarnings('error')
|
||||||
cum_returns(self.benchmark_returns).iloc[-1]
|
|
||||||
|
try:
|
||||||
|
self.benchmark_period_returns = \
|
||||||
|
cum_returns(self.benchmark_returns).iloc[-1]
|
||||||
|
except Exception:
|
||||||
|
# TODO: why is there an error
|
||||||
|
self.benchmark_period_returns = 0
|
||||||
|
|
||||||
self.algorithm_period_returns = \
|
self.algorithm_period_returns = \
|
||||||
cum_returns(self.algorithm_returns).iloc[-1]
|
cum_returns(self.algorithm_returns).iloc[-1]
|
||||||
|
|
||||||
if not self.algorithm_returns.index.equals(
|
if not self.algorithm_returns.index.equals(
|
||||||
self.benchmark_returns.index
|
self.benchmark_returns.index
|
||||||
):
|
):
|
||||||
message = "Mismatch between benchmark_returns ({bm_count}) and \
|
message = "Mismatch between benchmark_returns ({bm_count}) and \
|
||||||
algorithm_returns ({algo_count}) in range {start} : {end}"
|
algorithm_returns ({algo_count}) in range {start} : {end}"
|
||||||
@@ -128,10 +135,17 @@ class RiskMetricsPeriod(object):
|
|||||||
self.downside_risk = downside_risk(
|
self.downside_risk = downside_risk(
|
||||||
self.algorithm_returns.values
|
self.algorithm_returns.values
|
||||||
)
|
)
|
||||||
self.sortino = sortino_ratio(
|
|
||||||
self.algorithm_returns.values,
|
try:
|
||||||
_downside_risk=self.downside_risk,
|
risk = self.downside_risk
|
||||||
)
|
self.sortino = sortino_ratio(
|
||||||
|
self.algorithm_returns.values,
|
||||||
|
_downside_risk=risk,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# TODO: what causes it to error out?
|
||||||
|
self.sortino = 0
|
||||||
|
|
||||||
self.information = information_ratio(
|
self.information = information_ratio(
|
||||||
self.algorithm_returns.values,
|
self.algorithm_returns.values,
|
||||||
self.benchmark_returns.values,
|
self.benchmark_returns.values,
|
||||||
@@ -140,11 +154,13 @@ 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()
|
||||||
|
|
||||||
|
warnings.resetwarnings()
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
Creates a dictionary representing the state of the risk report.
|
Creates a dictionary representing the state of the risk report.
|
||||||
|
|||||||
@@ -160,7 +160,8 @@ def choose_treasury(select_treasury, treasury_curves, start_session,
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
if search_day and trading_calendar.name != 'OPEN': # Supress warning for 'OPEN' calendar
|
# 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,7 +41,6 @@ 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,6 +7,7 @@ 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,
|
||||||
@@ -33,7 +34,6 @@ 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,6 +118,7 @@ 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 "
|
||||||
@@ -484,8 +485,10 @@ 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(
|
||||||
loader_groups[loader_group_key(term)],
|
six.next(six.itervalues(loader_groups)),
|
||||||
key=lambda t: t.dataset
|
key=lambda t: t.dataset
|
||||||
)
|
)
|
||||||
loader = get_loader(term)
|
loader = get_loader(term)
|
||||||
@@ -565,9 +568,10 @@ class SimplePipelineEngine(PipelineEngine):
|
|||||||
index=MultiIndex.from_arrays([empty_dates, empty_assets]),
|
index=MultiIndex.from_arrays([empty_dates, empty_assets]),
|
||||||
)
|
)
|
||||||
|
|
||||||
resolved_assets = array(self._finder.retrieve_all(assets))
|
# TODO: not sure what's wrong with the resolved_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(resolved_assets, len(dates))[mask]
|
assets_kept = repeat_first_axis(assets, len(dates))[mask]
|
||||||
|
|
||||||
final_columns = {}
|
final_columns = {}
|
||||||
for name in data:
|
for name in data:
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
from .statistical import (
|
from .statistical import (
|
||||||
RollingPearson,
|
|
||||||
RollingLinearRegression,
|
|
||||||
RollingLinearRegressionOfReturns,
|
RollingLinearRegressionOfReturns,
|
||||||
RollingPearsonOfReturns,
|
RollingPearsonOfReturns,
|
||||||
RollingSpearman,
|
|
||||||
RollingSpearmanOfReturns,
|
RollingSpearmanOfReturns,
|
||||||
)
|
)
|
||||||
from .technical import (
|
from .technical import (
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ class TermGraph(object):
|
|||||||
at the end of execution.
|
at the end of execution.
|
||||||
"""
|
"""
|
||||||
refcounts = self.graph.out_degree()
|
refcounts = self.graph.out_degree()
|
||||||
for t in self.outputs.values():
|
for t in list(self.outputs.values()):
|
||||||
refcounts[t] += 1
|
refcounts[t] += 1
|
||||||
|
|
||||||
for t in initial_terms:
|
for t in initial_terms:
|
||||||
@@ -238,7 +238,7 @@ class ExecutionPlan(TermGraph):
|
|||||||
min_extra_rows=0):
|
min_extra_rows=0):
|
||||||
super(ExecutionPlan, self).__init__(terms)
|
super(ExecutionPlan, self).__init__(terms)
|
||||||
|
|
||||||
for term in terms.values():
|
for term in list(terms.values()):
|
||||||
self.set_extra_rows(
|
self.set_extra_rows(
|
||||||
term,
|
term,
|
||||||
all_dates,
|
all_dates,
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ class USEquityPricingLoader(PipelineLoader):
|
|||||||
|
|
||||||
def __init__(self, bundle, data_frequency, dataset):
|
def __init__(self, bundle, data_frequency, dataset):
|
||||||
|
|
||||||
if data_frequency == 'daily':
|
# TODO: This is currently broken, No Pipeline support for Catalyst
|
||||||
reader = bundle.daily_bar_reader
|
# if data_frequency == 'daily':
|
||||||
elif daily_bar_reader == 'minute':
|
# reader = bundle.daily_bar_reader
|
||||||
|
# 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(
|
||||||
@@ -51,7 +53,9 @@ class USEquityPricingLoader(PipelineLoader):
|
|||||||
|
|
||||||
if data_frequency == 'daily':
|
if data_frequency == 'daily':
|
||||||
all_sessions = cal.all_sessions
|
all_sessions = cal.all_sessions
|
||||||
elif daily_bar_reader == 'minute':
|
# TODO: this cannot be right, but no pipeline support at the moment
|
||||||
|
# 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,4 +180,3 @@ class DataFrameLoader(PipelineLoader):
|
|||||||
@property
|
@property
|
||||||
def columns(self):
|
def columns(self):
|
||||||
return self._columns
|
return self._columns
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class SpecificEquityTrades(object):
|
|||||||
for identifier in self.identifiers:
|
for identifier in self.identifiers:
|
||||||
assets_by_identifier[identifier] = env.asset_finder.\
|
assets_by_identifier[identifier] = env.asset_finder.\
|
||||||
lookup_generic(identifier, datetime.now())[0]
|
lookup_generic(identifier, datetime.now())[0]
|
||||||
self.sids = [asset.sid for asset in assets_by_identifier.values()]
|
self.sids = [asset.sid for asset in list(assets_by_identifier.values())]
|
||||||
for event in self.event_list:
|
for event in self.event_list:
|
||||||
event.sid = assets_by_identifier[event.sid].sid
|
event.sid = assets_by_identifier[event.sid].sid
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ class SpecificEquityTrades(object):
|
|||||||
for identifier in self.identifiers:
|
for identifier in self.identifiers:
|
||||||
assets_by_identifier[identifier] = env.asset_finder.\
|
assets_by_identifier[identifier] = env.asset_finder.\
|
||||||
lookup_generic(identifier, datetime.now())[0]
|
lookup_generic(identifier, datetime.now())[0]
|
||||||
self.sids = [asset.sid for asset in assets_by_identifier.values()]
|
self.sids = [asset.sid for asset in list(assets_by_identifier.values())]
|
||||||
|
|
||||||
# Hash_value for downstream sorting.
|
# Hash_value for downstream sorting.
|
||||||
self.arg_string = hash_args(*args, **kwargs)
|
self.arg_string = hash_args(*args, **kwargs)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.i = -1 # counts the minutes
|
||||||
|
context.exchange = 'cryptopia'
|
||||||
|
context.base_currency = 'btc'
|
||||||
|
context.coins = context.exchanges[context.exchange].assets
|
||||||
|
context.coins = [c for c in context.coins if
|
||||||
|
c.quote_currency == context.base_currency]
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# current date formatted into a string
|
||||||
|
today = data.current_dt
|
||||||
|
|
||||||
|
# update universe everyday
|
||||||
|
new_day = 60 * 24 # assuming data_frequency='minute'
|
||||||
|
if not context.i % new_day:
|
||||||
|
context.coins = context.exchanges[context.exchange].assets
|
||||||
|
context.coins = [c for c in context.coins if
|
||||||
|
c.quote_currency == context.base_currency]
|
||||||
|
|
||||||
|
# get data every 30 minutes
|
||||||
|
minutes = 1
|
||||||
|
if not context.i % minutes:
|
||||||
|
# we iterate for every pair in the current universe
|
||||||
|
for coin in context.coins:
|
||||||
|
pair = str(coin.symbol)
|
||||||
|
|
||||||
|
price = data.current(coin, 'price')
|
||||||
|
print(today, pair, price)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, results=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
start_date = pd.to_datetime('2018-01-17', utc=True)
|
||||||
|
end_date = pd.to_datetime('2018-01-18', utc=True)
|
||||||
|
|
||||||
|
performance = run_algorithm(
|
||||||
|
capital_base=1.0,
|
||||||
|
# amount of base_currency, not always in dollars unless usd
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='cryptopia',
|
||||||
|
data_frequency='minute',
|
||||||
|
base_currency='btc',
|
||||||
|
live=True,
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=True,
|
||||||
|
algo_namespace='simple_universe'
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import ccxt
|
||||||
|
|
||||||
|
bitfinex = ccxt.bitfinex()
|
||||||
|
bitfinex.verbose = True
|
||||||
|
ohlcvs = bitfinex.fetch_ohlcv('ETH/BTC', '30m', 1504224000000)
|
||||||
|
|
||||||
|
dt = bitfinex.iso8601(ohlcvs[0][0])
|
||||||
|
print(dt) # should print '2017-09-01T00:00:00.000Z'
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.asset1 = symbol('fct_btc')
|
||||||
|
context.asset2 = symbol('btc_usdt')
|
||||||
|
context.coins = [context.asset1, context.asset2]
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
df = data.history(context.coins,
|
||||||
|
'close',
|
||||||
|
bar_count=10,
|
||||||
|
frequency='5T',
|
||||||
|
)
|
||||||
|
print(df)
|
||||||
|
print(data.current(context.asset1, 'close'))
|
||||||
|
print(data.current(context.asset2, 'close'))
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
LIVE = True
|
||||||
|
if LIVE:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='test_multi_assets',
|
||||||
|
base_currency='usdt',
|
||||||
|
live=True,
|
||||||
|
simulate_orders=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='test_multi_assets',
|
||||||
|
base_currency='usdt',
|
||||||
|
live=False,
|
||||||
|
start=pd.to_datetime('2017-12-1', utc=True),
|
||||||
|
end=pd.to_datetime('2017-12-1', utc=True),
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import order_target_percent
|
||||||
|
|
||||||
|
NAMESPACE = 'goose7'
|
||||||
|
log = Logger(NAMESPACE)
|
||||||
|
|
||||||
|
from catalyst.api import record, symbol
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.asset = symbol('trx_btc')
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
price = data.current(context.asset, 'price')
|
||||||
|
record(btc=price)
|
||||||
|
|
||||||
|
# Only ordering if it does not have any position to avoid trying some
|
||||||
|
# tiny orders with the leftover btc
|
||||||
|
pos_amount = context.portfolio.positions[context.asset].amount
|
||||||
|
if pos_amount > 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Adding a limit price to workaround an issue with performance
|
||||||
|
# calculations of market orders
|
||||||
|
order_target_percent(
|
||||||
|
context.asset, 1, limit_price=price * 1.01
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.003,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
exchange_name='binance',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='btc',
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=False,
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.asset = symbol('btc_usdt')
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
df = data.history(context.asset,
|
||||||
|
'close',
|
||||||
|
bar_count=10,
|
||||||
|
frequency='5T',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
LIVE = True
|
||||||
|
if LIVE:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='test_algo',
|
||||||
|
base_currency='usdt',
|
||||||
|
live=True,
|
||||||
|
simulate_orders=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='test_algo',
|
||||||
|
base_currency='usdt',
|
||||||
|
live=False,
|
||||||
|
start=pd.to_datetime('2017-12-1', utc=True),
|
||||||
|
end=pd.to_datetime('2017-12-1', utc=True),
|
||||||
|
)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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),
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
from catalyst.api import symbol
|
||||||
|
from exchange.utils.stats_utils import set_print_settings
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.i = 0
|
||||||
|
context.data = []
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
prices = data.history(
|
||||||
|
symbol('xlm_eth'),
|
||||||
|
fields=['open', 'high', 'low', 'close'],
|
||||||
|
bar_count=50,
|
||||||
|
frequency='1T'
|
||||||
|
)
|
||||||
|
set_print_settings()
|
||||||
|
print(prices.tail(10))
|
||||||
|
context.data.append(prices)
|
||||||
|
|
||||||
|
context.i = context.i + 1
|
||||||
|
if context.i == 3:
|
||||||
|
context.interrupt_algorithm()
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context, prefs):
|
||||||
|
for dataset in context.data:
|
||||||
|
print(dataset[-2:])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.1,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='binance',
|
||||||
|
algo_namespace='Test candles',
|
||||||
|
base_currency='eth',
|
||||||
|
data_frequency='minute',
|
||||||
|
live=True,
|
||||||
|
simulate_orders=True)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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)
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
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')
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
"""
|
|
||||||
Requires Catalyst version 0.3.0 or above
|
|
||||||
Tested on Catalyst version 0.3.2
|
|
||||||
|
|
||||||
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 = 'eth' # 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.
|
|
||||||
open = fill(data.history(coin, 'open', bar_count=lookback,
|
|
||||||
frequency='1m')).resample('30T').first()
|
|
||||||
high = fill(data.history(coin, 'high', bar_count=lookback,
|
|
||||||
frequency='1m')).resample('30T').max()
|
|
||||||
low = fill(data.history(coin, 'low', bar_count=lookback,
|
|
||||||
frequency='1m')).resample('30T').min()
|
|
||||||
close = fill(data.history(coin, 'price', bar_count=lookback,
|
|
||||||
frequency='1m')).resample('30T').last()
|
|
||||||
volume = fill(data.history(coin, 'volume', bar_count=lookback,
|
|
||||||
frequency='1m')).resample('30T').sum()
|
|
||||||
|
|
||||||
# close[-1] is the equivalent to current price
|
|
||||||
# displays the minute price for each pair every 30 minutes
|
|
||||||
print(
|
|
||||||
today, pair, open[-1], high[-1], low[-1], close[-1], volume[-1])
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------------------------------------
|
|
||||||
# -------------------------------------- Insert Your Strategy Here -----------------------------------------
|
|
||||||
# ----------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def analyze(context=None, results=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# Get the universe for a given exchange and a given base_currency market
|
|
||||||
# Example: Poloniex BTC Market
|
|
||||||
def universe(context, lookback_date, current_date):
|
|
||||||
json_symbols = get_exchange_symbols(
|
|
||||||
context.exchange) # get all the pairs for the exchange
|
|
||||||
universe_df = pd.DataFrame.from_dict(json_symbols).transpose().astype(
|
|
||||||
str) # convert into a dataframe
|
|
||||||
universe_df['base_currency'] = universe_df.apply(
|
|
||||||
lambda row: row.symbol.split('_')[1],
|
|
||||||
axis=1)
|
|
||||||
universe_df['market_currency'] = universe_df.apply(
|
|
||||||
lambda row: row.symbol.split('_')[0],
|
|
||||||
axis=1)
|
|
||||||
# Filter all the exchange pairs to only the ones for a give base currency
|
|
||||||
universe_df = universe_df[
|
|
||||||
universe_df['base_currency'] == context.base_currency]
|
|
||||||
|
|
||||||
# Filter all the pairs to ensure that pair existed in the current date range
|
|
||||||
universe_df = universe_df[universe_df.start_date < lookback_date]
|
|
||||||
universe_df = universe_df[universe_df.end_daily >= current_date]
|
|
||||||
context.coins = symbols(
|
|
||||||
*universe_df.symbol) # convert all the pairs to symbols
|
|
||||||
print(universe_df.head(), len(universe_df))
|
|
||||||
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-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='eth',
|
|
||||||
live=False,
|
|
||||||
live_graph=False,
|
|
||||||
algo_namespace='simple_universe')
|
|
||||||
|
|
||||||
"""
|
|
||||||
Run in Terminal (inside catalyst environment):
|
|
||||||
python simple_universe.py
|
|
||||||
"""
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import talib
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
from catalyst import run_algorithm
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
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'
|
|
||||||
)
|
|
||||||
@@ -31,4 +31,5 @@ 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__(start=Timestamp('2015-3-1', tz='UTC'), **kwargs)
|
super(OpenExchangeCalendar, self).__init__(
|
||||||
|
start=Timestamp('2015-3-1', tz='UTC'), **kwargs)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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:
|
||||||
@@ -17,12 +18,13 @@ 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,
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ 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
|
||||||
|
|
||||||
|
|||||||
+116
-117
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -7,10 +8,12 @@ from time import sleep
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from catalyst.data.bundles import load
|
||||||
from catalyst.exchange.bittrex.bittrex import Bittrex
|
from catalyst.data.data_portal import DataPortal
|
||||||
from catalyst.exchange.bitfinex.bitfinex import Bitfinex
|
from catalyst.exchange.exchange_pricing_loader import ExchangePricingLoader, \
|
||||||
from catalyst.exchange.poloniex.poloniex import Poloniex
|
TradingPairPricing
|
||||||
|
from catalyst.exchange.utils.factory import get_exchange
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pygments import highlight
|
from pygments import highlight
|
||||||
@@ -29,19 +32,13 @@ 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 ExchangeTradingAlgorithmLive, \
|
from catalyst.exchange.exchange_algorithm import (
|
||||||
ExchangeTradingAlgorithmBacktest
|
ExchangeTradingAlgorithmLive,
|
||||||
|
ExchangeTradingAlgorithmBacktest,
|
||||||
|
)
|
||||||
from catalyst.exchange.exchange_data_portal import DataPortalExchangeLive, \
|
from catalyst.exchange.exchange_data_portal import DataPortalExchangeLive, \
|
||||||
DataPortalExchangeBacktest
|
DataPortalExchangeBacktest
|
||||||
from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
|
from catalyst.exchange.exchange_asset_finder import ExchangeAssetFinder
|
||||||
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
|
||||||
from catalyst.exchange.exchange_errors import (
|
|
||||||
ExchangeRequestError, ExchangeAuthEmpty,
|
|
||||||
ExchangeRequestErrorTooManyAttempts,
|
|
||||||
BaseCurrencyNotFoundError, ExchangeNotFoundError)
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_auth, \
|
|
||||||
get_algo_object, get_exchange_folder
|
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
@@ -91,7 +88,11 @@ def _run(handle_data,
|
|||||||
exchange,
|
exchange,
|
||||||
algo_namespace,
|
algo_namespace,
|
||||||
base_currency,
|
base_currency,
|
||||||
live_graph):
|
live_graph,
|
||||||
|
analyze_live,
|
||||||
|
simulate_orders,
|
||||||
|
auth_aliases,
|
||||||
|
stats_output):
|
||||||
"""Run a backtest for the given algorithm.
|
"""Run a backtest for the given algorithm.
|
||||||
|
|
||||||
This is shared between the cli and :func:`catalyst.run_algo`.
|
This is shared between the cli and :func:`catalyst.run_algo`.
|
||||||
@@ -140,7 +141,22 @@ def _run(handle_data,
|
|||||||
else:
|
else:
|
||||||
click.echo(algotext)
|
click.echo(algotext)
|
||||||
|
|
||||||
mode = 'live' if live else 'backtest'
|
log.warn(
|
||||||
|
'Catalyst is currently in ALPHA. It is going through rapid '
|
||||||
|
'development and it is subject to errors. Please use carefully. '
|
||||||
|
'We encourage you to report any issue on GitHub: '
|
||||||
|
'https://github.com/enigmampc/catalyst/issues'
|
||||||
|
)
|
||||||
|
sleep(3)
|
||||||
|
|
||||||
|
if live:
|
||||||
|
if simulate_orders:
|
||||||
|
mode = 'paper-trading'
|
||||||
|
else:
|
||||||
|
mode = 'live-trading'
|
||||||
|
else:
|
||||||
|
mode = 'backtest'
|
||||||
|
|
||||||
log.info('running algo in {mode} mode'.format(mode=mode))
|
log.info('running algo in {mode} mode'.format(mode=mode))
|
||||||
|
|
||||||
exchange_name = exchange
|
exchange_name = exchange
|
||||||
@@ -148,53 +164,20 @@ def _run(handle_data,
|
|||||||
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 = dict()
|
exchanges = dict()
|
||||||
for exchange_name in exchange_list:
|
for name in exchange_list:
|
||||||
|
if auth_aliases is not None and name in auth_aliases:
|
||||||
# Looking for the portfolio from the cache first
|
auth_alias = auth_aliases[name]
|
||||||
portfolio = get_algo_object(
|
|
||||||
algo_name=algo_namespace,
|
|
||||||
key='portfolio_{}'.format(exchange_name),
|
|
||||||
environ=environ
|
|
||||||
)
|
|
||||||
|
|
||||||
if portfolio is None:
|
|
||||||
portfolio = ExchangePortfolio(
|
|
||||||
start_date=pd.Timestamp.utcnow()
|
|
||||||
)
|
|
||||||
|
|
||||||
# This corresponds to the json file containing api token info
|
|
||||||
exchange_auth = get_exchange_auth(exchange_name)
|
|
||||||
|
|
||||||
if live and (exchange_auth['key'] == '' or exchange_auth['secret'] == ''):
|
|
||||||
raise ExchangeAuthEmpty(
|
|
||||||
exchange=exchange_name.title(),
|
|
||||||
filename=os.path.join(get_exchange_folder(exchange_name, environ), 'auth.json') )
|
|
||||||
|
|
||||||
if exchange_name == 'bitfinex':
|
|
||||||
exchanges[exchange_name] = Bitfinex(
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'],
|
|
||||||
base_currency=base_currency,
|
|
||||||
portfolio=portfolio
|
|
||||||
)
|
|
||||||
elif exchange_name == 'bittrex':
|
|
||||||
exchanges[exchange_name] = Bittrex(
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'],
|
|
||||||
base_currency=base_currency,
|
|
||||||
portfolio=portfolio
|
|
||||||
)
|
|
||||||
elif exchange_name == 'poloniex':
|
|
||||||
exchanges[exchange_name] = Poloniex(
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'],
|
|
||||||
base_currency=base_currency,
|
|
||||||
portfolio=portfolio
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise ExchangeNotFoundError(exchange_name=exchange_name)
|
auth_alias = None
|
||||||
|
|
||||||
|
exchanges[name] = get_exchange(
|
||||||
|
exchange_name=name,
|
||||||
|
base_currency=base_currency,
|
||||||
|
must_authenticate=(live and not simulate_orders),
|
||||||
|
skip_init=True,
|
||||||
|
auth_alias=auth_alias,
|
||||||
|
)
|
||||||
|
|
||||||
open_calendar = get_calendar('OPEN')
|
open_calendar = get_calendar('OPEN')
|
||||||
|
|
||||||
@@ -209,14 +192,22 @@ def _run(handle_data,
|
|||||||
exchange_tz='UTC',
|
exchange_tz='UTC',
|
||||||
asset_db_path=None # We don't need an asset db, we have exchanges
|
asset_db_path=None # We don't need an asset db, we have exchanges
|
||||||
)
|
)
|
||||||
env.asset_finder = AssetFinderExchange()
|
env.asset_finder = ExchangeAssetFinder(exchanges=exchanges)
|
||||||
choose_loader = None # TODO: use the DataPortal for in the algorithm class for this
|
|
||||||
|
def choose_loader(column):
|
||||||
|
bound_cols = TradingPairPricing.columns
|
||||||
|
if column in bound_cols:
|
||||||
|
return ExchangePricingLoader(data_frequency)
|
||||||
|
raise ValueError(
|
||||||
|
"No PipelineLoader registered for column %s." % column
|
||||||
|
)
|
||||||
|
|
||||||
if live:
|
if live:
|
||||||
start = pd.Timestamp.utcnow()
|
start = pd.Timestamp.utcnow()
|
||||||
|
|
||||||
# TODO: fix the end data.
|
# TODO: fix the end data.
|
||||||
end = start + timedelta(hours=8760)
|
if end is None:
|
||||||
|
end = start + timedelta(hours=8760)
|
||||||
|
|
||||||
data = DataPortalExchangeLive(
|
data = DataPortalExchangeLive(
|
||||||
exchanges=exchanges,
|
exchanges=exchanges,
|
||||||
@@ -225,51 +216,6 @@ def _run(handle_data,
|
|||||||
first_trading_day=pd.to_datetime('today', utc=True)
|
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:
|
|
||||||
return balances[base_currency]
|
|
||||||
else:
|
|
||||||
raise BaseCurrencyNotFoundError(
|
|
||||||
base_currency=base_currency,
|
|
||||||
exchange=exchange_name
|
|
||||||
)
|
|
||||||
|
|
||||||
capital_base = 0
|
|
||||||
for exchange_name in exchanges:
|
|
||||||
exchange = exchanges[exchange_name]
|
|
||||||
capital_base += fetch_capital_base(exchange)
|
|
||||||
|
|
||||||
sim_params = create_simulation_parameters(
|
sim_params = create_simulation_parameters(
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
@@ -285,9 +231,13 @@ def _run(handle_data,
|
|||||||
ExchangeTradingAlgorithmLive,
|
ExchangeTradingAlgorithmLive,
|
||||||
exchanges=exchanges,
|
exchanges=exchanges,
|
||||||
algo_namespace=algo_namespace,
|
algo_namespace=algo_namespace,
|
||||||
live_graph=live_graph
|
live_graph=live_graph,
|
||||||
|
simulate_orders=simulate_orders,
|
||||||
|
stats_output=stats_output,
|
||||||
|
analyze_live=analyze_live,
|
||||||
|
end=end,
|
||||||
)
|
)
|
||||||
else:
|
elif exchanges:
|
||||||
# Removed the existing Poloniex fork to keep things simple
|
# Removed the existing Poloniex fork to keep things simple
|
||||||
# We can add back the complexity if required.
|
# We can add back the complexity if required.
|
||||||
|
|
||||||
@@ -297,7 +247,7 @@ def _run(handle_data,
|
|||||||
# can handle this later.
|
# can handle this later.
|
||||||
|
|
||||||
data = DataPortalExchangeBacktest(
|
data = DataPortalExchangeBacktest(
|
||||||
exchanges=exchanges,
|
exchange_names=[exchange_name for exchange_name in exchanges],
|
||||||
asset_finder=None,
|
asset_finder=None,
|
||||||
trading_calendar=open_calendar,
|
trading_calendar=open_calendar,
|
||||||
first_trading_day=start,
|
first_trading_day=start,
|
||||||
@@ -317,6 +267,36 @@ def _run(handle_data,
|
|||||||
exchanges=exchanges
|
exchanges=exchanges
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif bundle is not None:
|
||||||
|
bundle_data = load(
|
||||||
|
bundle,
|
||||||
|
environ,
|
||||||
|
bundle_timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
|
prefix, connstr = re.split(
|
||||||
|
r'sqlite:///',
|
||||||
|
str(bundle_data.asset_finder.engine.url),
|
||||||
|
maxsplit=1,
|
||||||
|
)
|
||||||
|
if prefix:
|
||||||
|
raise ValueError(
|
||||||
|
"invalid url %r, must begin with 'sqlite:///'" %
|
||||||
|
str(bundle_data.asset_finder.engine.url),
|
||||||
|
)
|
||||||
|
|
||||||
|
env = TradingEnvironment(asset_db_path=connstr, environ=environ)
|
||||||
|
first_trading_day = \
|
||||||
|
bundle_data.equity_minute_bar_reader.first_trading_day
|
||||||
|
|
||||||
|
data = DataPortal(
|
||||||
|
env.asset_finder, open_calendar,
|
||||||
|
first_trading_day=first_trading_day,
|
||||||
|
equity_minute_reader=bundle_data.equity_minute_bar_reader,
|
||||||
|
equity_daily_reader=bundle_data.equity_daily_bar_reader,
|
||||||
|
adjustment_reader=bundle_data.adjustment_reader,
|
||||||
|
)
|
||||||
|
|
||||||
perf = algorithm_class(
|
perf = algorithm_class(
|
||||||
namespace=namespace,
|
namespace=namespace,
|
||||||
env=env,
|
env=env,
|
||||||
@@ -416,7 +396,12 @@ def run_algorithm(initialize,
|
|||||||
exchange_name=None,
|
exchange_name=None,
|
||||||
base_currency=None,
|
base_currency=None,
|
||||||
algo_namespace=None,
|
algo_namespace=None,
|
||||||
live_graph=False):
|
live_graph=False,
|
||||||
|
analyze_live=None,
|
||||||
|
simulate_orders=True,
|
||||||
|
auth_aliases=None,
|
||||||
|
stats_output=None,
|
||||||
|
output=os.devnull):
|
||||||
"""Run a trading algorithm.
|
"""Run a trading algorithm.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -486,8 +471,18 @@ def run_algorithm(initialize,
|
|||||||
--------
|
--------
|
||||||
catalyst.data.bundles.bundles : The available data bundles.
|
catalyst.data.bundles.bundles : The available data bundles.
|
||||||
"""
|
"""
|
||||||
load_extensions(default_extension, extensions, strict_extensions, environ)
|
load_extensions(
|
||||||
|
default_extension, extensions, strict_extensions, environ
|
||||||
|
)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
@@ -527,7 +522,7 @@ def run_algorithm(initialize,
|
|||||||
bundle_timestamp=bundle_timestamp,
|
bundle_timestamp=bundle_timestamp,
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
output=os.devnull,
|
output=output,
|
||||||
print_algo=False,
|
print_algo=False,
|
||||||
local_namespace=False,
|
local_namespace=False,
|
||||||
environ=environ,
|
environ=environ,
|
||||||
@@ -535,5 +530,9 @@ def run_algorithm(initialize,
|
|||||||
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,
|
||||||
|
analyze_live=analyze_live,
|
||||||
|
simulate_orders=simulate_orders,
|
||||||
|
auth_aliases=auth_aliases,
|
||||||
|
stats_output=stats_output
|
||||||
)
|
)
|
||||||
|
|||||||
+16188
-149
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