Compare commits

...
335 Commits
Author SHA1 Message Date
Victor Grau Serrat 3399e22ea2 MAINT: [0.3.10] updated generate_symbols_json 2018-03-22 10:28:01 -06:00
Victor Grau Serrat 79e5dec813 BUG: minor fix in Poloniex curate script 2018-01-04 14:18:27 +00:00
fredfortier 803823eac0 merging from develop 2017-11-28 01:41:58 -05:00
fredfortier 5a18e09730 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	docs/source/releases.rst
2017-11-28 01:34:38 -05:00
fredfortier dd41f8c006 BUG: fixed issue with daily frequency 2017-11-28 01:34:17 -05:00
Victor Grau Serrat 7a2a4817fe BUG: missing parameters in log statements 2017-11-27 23:11:22 -07:00
Victor Grau Serrat 105522e5ab DOC: fixed date from 0.3.9 release 2017-11-27 22:01:19 -07:00
Victor Grau Serrat a52e201f86 DOC: improved dual_moving_average.py example algo 2017-11-27 21:10:17 -07:00
Victor Grau Serrat 697ff54125 Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-11-27 21:07:15 -07:00
Victor Grau Serrat 4bcd34bd78 DOC: improved beginner tutorial 2017-11-27 21:07:03 -07:00
fredfortier 64c52c7a3c BUG: fixed issue #72 with the buy_and_hodl sample algo 2017-11-27 18:44:50 -05:00
fredfortier 1c143eb9ea DOC: 0.3.9 release notes 2017-11-27 17:55:56 -05:00
fredfortier 1da4ccfb8c Merge remote-tracking branch 'origin/master' 2017-11-27 17:55:32 -05:00
fredfortier a61b22b821 DOC: 0.3.9 release notes 2017-11-27 17:55:22 -05:00
fredfortier d07e0edd88 DOC: 0.3.9 release notes 2017-11-27 17:54:34 -05:00
Victor Grau Serrat c2821ab77b DOC: added example_algo: Dual Moving Average Crossover 2017-11-27 15:43:57 -07:00
fredfortier 1696db930d Merge branch 'develop' 2017-11-27 17:37:15 -05:00
fredfortier 9397b3fd5a BLD: testing simple universe with Bitfinex 2017-11-27 17:36:44 -05:00
fredfortier 2bb11db412 BLD: modified sample algo for testing 2017-11-27 17:28:13 -05:00
fredfortier c2f3e00d99 BUG: Adding back missing constants 2017-11-27 17:27:43 -05:00
fredfortier 292fe66d3f Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	catalyst/constants.py
2017-11-27 17:17:05 -05:00
fredfortier ba46015bae BLD: completed implementation of issue #65, support for custom exchange data 2017-11-27 17:16:44 -05:00
Victor Grau Serrat 12d5915c8e ENH: DEBUG level can be easily overriden from the local environment 2017-11-27 15:02:13 -07:00
Victor Grau Serrat c6fe45371c ENH: removed default for --capital_base in backtesting 2017-11-27 13:20:00 -07:00
fredfortier 968e70b69b BLD: implementing issue #65, implemented custom exchange data 2017-11-25 07:43:20 -05:00
fredfortier 6a7c47f3a9 BLD: implementing issue #65, adding local symbols definition 2017-11-24 01:58:33 -05:00
fredfortier 7daf295e63 BLD: refactoring to decrease reliance on the Exchange in preparation to support ad-hoc CSV bundles 2017-11-23 22:11:23 -05:00
fredfortier 7dddc0a85f BUG: fixed issue #80 but updated performance stats immediately after registering transactions in live mode 2017-11-22 21:11:16 -05:00
fredfortier 32523d474d Merge remote-tracking branch 'origin/develop' into develop 2017-11-22 16:03:29 -05:00
fredfortier 841acf0203 BLD: implemented issue #79, using the capital_base parameter to override the amount of base currency available for trading 2017-11-22 16:03:21 -05:00
Victor Grau Serrat b4ab1a5375 DOC: remake of beginner tutorial 2017-11-21 22:53:50 -07:00
fredfortier 3ec9853b75 BUG: in relation to issue #77, catching the remaining warnings 2017-11-21 20:42:44 -05:00
fredfortier c1d140a831 BUG: fixed issue #77, a sortino warning prevents analyze() from completing 2017-11-21 15:55:56 -05:00
fredfortier 02dc4d6a30 BUG: made some live-trading adjustments related to issue #71 2017-11-21 13:56:05 -05:00
fredfortier 0d366a350d BUG: fixed #75, adjusted the ruturn value of run_algorithm to support minute stats. 2017-11-20 21:53:48 -05:00
fredfortier 1e8b0c36a1 BUG: fixed #74, a problematic scenario when retrieving the history of multiple assets. 2017-11-20 20:00:48 -05:00
fredfortier 0af592a5f4 BUG: fixed issue #71 with the last candle of a resampled set 2017-11-20 17:52:29 -05:00
Victor Grau Serrat 86b2a5c772 DOC: videos: +3rd_install, +backtest 2017-11-20 09:31:47 -07:00
Victor Grau Serrat 9cfd50dc4f DOC: mean_reversion_simple.py minor edits, and added to doc website 2017-11-20 09:12:43 -07:00
Victor Grau Serrat 698b19c8fa DOC: updated examples/buy_and_hodl.py. Added Example Algos and Utilities pages to the documentation 2017-11-19 23:51:57 -07:00
Victor Grau Serrat 5d4bc99097 Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-11-19 22:04:28 -07:00
Victor Grau Serrat cfb3f1ca42 DOC: restructured install page 2017-11-19 22:04:11 -07:00
fredfortier ee1605a5e6 Merge branch 'abnera-patch-2' into develop 2017-11-17 19:43:46 -05:00
fredfortier f3dca74e87 BUG: fixed a get_candles issue with the Poloniex exchange 2017-11-17 19:40:49 -05:00
Victor Grau Serrat d57b79427b BUG: enforced --capital base in backtesting 2017-11-17 10:43:20 -07:00
Victor Grau Serrat 8a89c0c53f BUG: enforced --base_currency in backtesting. Fixes #67. 2017-11-17 09:52:25 -07:00
fredfortier c260e188b0 BUG: looking a potential resampling issue 2017-11-16 18:14:24 -05:00
fredfortier 230b9c17eb Merge branch 'patch-2' of https://github.com/abnera/catalyst into abnera-patch-2 2017-11-16 16:58:15 -05:00
fredfortier 2a8b5cf911 Merge branch 'damo1884-talib_example' into develop 2017-11-16 16:56:07 -05:00
fredfortier 3fa88a3e56 BLD: misc housekeeping 2017-11-16 16:55:40 -05:00
fredfortier 64532c3d08 BLD: minor adjustments to the talib sample algo 2017-11-16 16:54:32 -05:00
fredfortier 5f86ab659e Merge branch 'talib_example' of https://github.com/damo1884/catalyst into damo1884-talib_example 2017-11-16 16:48:10 -05:00
Abner Ayala-AcevedoandGitHub df14a94918 Modified for examples consistency.
Fully tested on v0.3.8
2017-11-16 11:22:35 -08:00
fredfortier e087e48088 BLD: polishing a sample algorithm 2017-11-14 17:04:38 -05:00
fredfortier 5110b37a82 Merge remote-tracking branch 'origin/develop' into develop 2017-11-14 16:39:19 -05:00
Victor Grau Serrat a2bb231424 DOC: improving Win/Conda install instructions 2017-11-14 12:14:38 -07:00
fredfortier e939f742a8 Merge branch 'master' into develop 2017-11-14 13:59:17 -05:00
fredfortier 9093be748e BUG: fixed a warning filter issue 2017-11-14 13:58:24 -05:00
fredfortier e23a7e67a0 merging from the develop branch 2017-11-14 13:29:50 -05:00
fredfortier 273b4fb7a7 DOC: updated release notes 2017-11-14 13:27:20 -05:00
fredfortier 0b2684d532 BLD: polishing a sample algorithm 2017-11-14 13:22:27 -05:00
fredfortier 224192a1ee BUG: fixed issue #63 warnings with cumulative metrics 2017-11-14 11:50:38 -05:00
fredfortier 2d7202ac81 BUG: fixed issue #64 with SSL certificates 2017-11-14 11:40:11 -05:00
fredfortier 8d95428fa6 BLD: created a simpler mean-reversion algo for the video 2017-11-14 11:01:46 -05:00
fredfortier d678376d8d BLD: polishing a sample algorithm 2017-11-14 02:28:22 -05:00
fredfortier 9b5fa83da3 BLD: polishing a sample algorithm 2017-11-14 01:00:21 -05:00
fredfortier 0f1c3e1ace Merge remote-tracking branch 'origin/develop' into develop 2017-11-13 22:06:21 -05:00
fredfortier f3cb610748 BLD: polishing a sample algorithm 2017-11-13 22:06:06 -05:00
Victor Grau Serrat 1e6316d414 BUG: PoloniexCurator: connection retries when fetching data 2017-11-13 15:31:49 -07:00
fredfortier df51cbe21b Merge remote-tracking branch 'origin/develop' into develop 2017-11-13 16:57:46 -05:00
fredfortier dce31b212b BLD: polishing a sample algorithm 2017-11-13 16:57:37 -05:00
Victor Grau Serrat 00269d3dfb MAINT: PoloniexCurator PEP8 edits 2017-11-13 14:41:10 -07:00
fredfortier 648be3969a DOC: added release notes of upcoming 0.3.7 release 2017-11-11 18:09:24 -05:00
fredfortier a54325fdcf BLD: issue #62, the stats now align with the data_frequency selected in the algo 2017-11-10 19:59:42 -05:00
fredfortier b64e5929b4 BUG: resolve issue #61 by adjusting our perf conventions to match zipline exactly. 2017-11-10 17:39:36 -05:00
fredfortier 631cbcd352 BLD: Working on the sample algo for intro videos. Made auto-ingestion configurable. 2017-11-09 19:56:57 -05:00
fredfortier 24c5a5bd13 Merge remote-tracking branch 'origin/develop' into develop 2017-11-09 17:10:06 -05:00
fredfortier 1103947af0 BLD: created a new sample algo for instructional materials. Fixed some minor issues in the process. 2017-11-09 17:09:55 -05:00
damo1884 061de3c12f fix issue with candlestick chart 2017-11-08 19:28:44 -08:00
lacabra 207887a28d MAINT: PoloniexCurator cleanup 2017-11-08 20:43:40 +00:00
Victor Grau Serrat dc53f973e4 Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-11-08 11:32:00 -07:00
Victor Grau Serrat 9a80a488cd MAINT: handful of coins with first tradeID > 1 and PEP8 2017-11-08 11:31:56 -07:00
fredfortier a85b6c798a BUG: fixes related to issue #47 and added a verbose ingestion option. 2017-11-07 13:42:47 -05:00
Victor Grau Serrat 9229809b05 DOC: fix broken documentation 2017-11-06 16:41:04 -07:00
fredfortier f81cf6b600 BUG: working on issue #57 2017-11-06 15:07:24 -05:00
fredfortier ba4ffc7272 BUG: working on issue #57 2017-11-06 15:04:44 -05:00
damo1884 12695474e3 Add TALib Simple Example 2017-11-05 01:14:09 -07:00
fredfortier 9d1dd5829d Merge branch 'develop' 2017-11-04 16:48:59 -04:00
fredfortier d4148891fc DOC: updated release notes prior to release 2017-11-04 16:43:27 -04:00
fredfortier c9c16f54b1 BUG: fixed on issue #55 with single history bar 2017-11-04 16:38:54 -04:00
fredfortier 7da72fe9cb BUG: working on issue #55 2017-11-04 16:22:10 -04:00
fredfortier 515c6e13f0 Merge branch 'develop' 2017-11-04 15:01:03 -04:00
fredfortier 5abdc063eb BLD: resolving conflict with algo 2017-11-04 14:57:54 -04:00
fredfortier 360e1adc22 BLD: resolving conflict with algo 2017-11-04 14:56:24 -04:00
fredfortier 8c6ac53a05 BLD: cleanup in algos and unit tests 2017-11-04 14:46:09 -04:00
fredfortier b636edb32f BLD: working on the sample algos 2017-11-04 14:16:25 -04:00
fredfortier d02c6d8ce9 BLD: optimize imports 2017-11-03 21:04:16 -04:00
fredfortier 88f6557aaf Merge remote-tracking branch 'origin/develop' into develop 2017-11-03 20:59:42 -04:00
fredfortier b476024612 BUG: work around for issue #53 and possibly fixed issue #47 2017-11-03 20:59:26 -04:00
Victor Grau Serrat a3808c31ef DOC: releases 2017-11-02 23:53:21 -05:00
fredfortier 5d251f6f9a BLD: modified algo for testing 2017-11-02 21:06:11 -04:00
fredfortier 117332d0b4 Merge branch 'develop' 2017-11-02 20:46:06 -04:00
fredfortier 2bbc0c00cc BLD: updating test algo 2017-11-02 20:44:16 -04:00
fredfortier 5e4ad9b338 BUG: accounting for daily historical bars with minute freq algo 2017-11-02 20:18:34 -04:00
fredfortier a9a422c892 DOC: updating the code docstrings 2017-11-01 23:10:31 -04:00
fredfortier 5b6bbacab0 DOC: updating the code docstrings 2017-11-01 21:31:51 -04:00
fredfortier 35677c553c BUG: fixed issues with data frequencies in data.history() which was particularly noticeable in live mode and minor adjustments around the commission model 2017-10-31 23:34:48 -04:00
fredfortier df357d2327 BUG: reduced the commission and slippage values to account for lower volume transactions. These models are still simple approximations. More work required to closely model exchange fees. (fixing previous commit) 2017-10-31 21:43:24 -04:00
fredfortier e6ff7ee4fc BUG: reduced the commission and slippage values to account for lower volume transactions. These models are still simple approximations. More work required to closely model exchange fees. 2017-10-31 21:40:12 -04:00
fredfortier 30eea4b8f7 Merge remote-tracking branch 'origin/develop' into develop 2017-10-31 19:22:46 -04:00
fredfortier 7ad047a432 BUG: fixed an issue with can_trade() 2017-10-31 19:20:39 -04:00
Victor Grau Serrat e291a260b2 Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-10-31 13:56:27 -06:00
Victor Grau Serrat 9a9e66b43d DOC: jupyter notebook, expanded welcome & improved install notes 2017-10-31 13:56:16 -06:00
fredfortier 1c3deb648a DOC: updated release notes for 0.3.4 and 0.3 2017-10-31 15:21:40 -04:00
Victor Grau Serrat b39311de85 DOC: Resources page 2017-10-31 12:39:35 -06:00
Victor Grau Serrat 5417a0cdcf DOC: Release Notes 2017-10-31 12:12:28 -06:00
Victor Grau Serrat 6a4ea43d27 DOC: updated README 2017-10-31 09:51:37 -06:00
Victor Grau Serrat 7fc1ade46c Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-10-31 00:05:55 -06:00
Victor Grau Serrat 635fc80ef2 DOC: fix Windows conda install 2017-10-31 00:05:48 -06:00
fredfortier c59d805717 Merge remote-tracking branch 'origin/develop' into develop 2017-10-30 21:18:03 -04:00
fredfortier 3394614ecf BUG: Fixes issue #47. Made improvements around auto-ingestion. 2017-10-30 21:17:53 -04:00
Victor Grau Serrat 06c8ab9c37 DOC: troubleshooting Windows install 2017-10-30 15:57:19 -06:00
Victor Grau Serrat 6a0d0a0422 DOC: jupyter notebook fix 2017-10-30 15:14:27 -06:00
Victor Grau Serrat 9470771561 Merge branch 'master' into develop 2017-10-30 15:08:11 -06:00
Victor Grau Serrat 8132e1f5ea DOC: small fixes 2017-10-30 14:47:15 -06:00
Victor Grau Serrat 0b28bf0e96 DOC: live trading 2017-10-30 14:22:55 -06:00
Victor Grau Serrat 13f023d364 DOC: documenting the documentation 2017-10-30 13:54:27 -06:00
Victor Grau Serrat eaefe4a908 DOC: videos 2017-10-30 13:27:52 -06:00
Victor Grau Serrat f47b657c6f fix conda install 2017-10-30 11:57:26 -06:00
Victor Grau Serrat 800a2efa50 Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-10-30 11:16:33 -06:00
Victor Grau Serrat 7465e8e432 DOC: videos 2017-10-30 11:16:20 -06:00
Victor Grau Serrat 86ab3804e5 DOC: install 2017-10-27 10:46:14 -06:00
Victor Grau Serrat b749d47a61 FIX: DOCS install 2017-10-27 10:31:14 -06:00
fredfortier 032c7fd16b Improved frequency support for data.history() in backtest, standardized class names, improved unit tests and working on new sample algo. 2017-10-27 00:57:52 -04:00
fredfortier b9579ab4b4 Improved frequency support for data.history() in backtest, standardized class names, improved unit tests and working on new sample algo. 2017-10-27 00:53:19 -04:00
fredfortier c7632b57a6 Merge remote-tracking branch 'origin/develop' into develop 2017-10-27 00:30:36 -04:00
fredfortier da17e66961 Fixed major issue with sell orders 2017-10-27 00:30:26 -04:00
Abner Ayala-AcevedoandGitHub cd0157347f Refactoring 2017-10-26 18:10:12 -07:00
Abner Ayala-AcevedoandGitHub 76a8362e3d Update simple_universe.py 2017-10-26 15:17:02 -07:00
Abner Ayala-AcevedoandGitHub c8cc2edd36 Convert to 30 minutes ohlcv data 2017-10-26 15:16:13 -07:00
Victor Grau Serrat 5f8016c67e improving buy_btc_simple.py example 2017-10-26 14:08:47 -06:00
Victor Grau Serrat 3d88d6a2c7 Merge branch 'develop' - Release 0.3.3 2017-10-26 13:13:42 -06:00
Victor Grau Serrat 9c3a9e233b Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-10-26 12:55:32 -06:00
Victor Grau Serrat c43509c28e catching missing -x in ingest-exchange 2017-10-26 12:55:18 -06:00
fredfortier 0e0bfc82b5 Fixed issues in the prepare_chunk logic 2017-10-26 14:04:30 -04:00
fredfortier 2f660db511 Fixed an issue with daily chunks end date 2017-10-26 13:52:37 -04:00
fredfortier fdc5a30060 Added data validation unit tests and minor fixes to the get_candles method of Poloniex. 2017-10-26 02:33:17 -04:00
fredfortier bb1d96ed5d Merge remote-tracking branch 'origin/develop' into develop 2017-10-25 19:44:05 -04:00
fredfortier 59501905ab Poloniex get_candles fix and created a unit test to validate data. 2017-10-25 19:43:57 -04:00
Abner Ayala-AcevedoandGitHub cb870422c3 Create simple_universe.py
This example aims to help users get familiar with catalyst API's to collect and handle data.
2017-10-25 12:11:40 -07:00
Victor Grau Serrat 2b85732e36 Merge branch 'develop' - Release 0.3.2 2017-10-24 21:59:53 -06:00
Victor Grau Serrat 284c749bb5 Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-10-24 21:58:47 -06:00
VictorandGitHub d7f5e73f84 Merge pull request #43 from reinka/develop
[MIG] Migrated buy_and_hodl and buy_low_sell_high to version 0.3 to work with Poloniex exchange
2017-10-24 21:58:22 -06:00
Victor Grau Serrat cde69da173 Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-10-24 21:55:25 -06:00
Victor Grau Serrat bcc75f6b00 FIX: Poloniex 1min curator 2017-10-24 21:54:59 -06:00
fredfortier f179381b64 Small python 3 fixes 2017-10-24 23:41:37 -04:00
fredfortier 10ba53b897 Merge remote-tracking branch 'origin/develop' into develop 2017-10-24 20:03:58 -04:00
fredfortier 1cfe3b1bb2 Fixed issues in the prepare_chunk logic 2017-10-24 20:03:50 -04:00
Victor Grau Serrat 268ff9c826 Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-10-24 17:44:24 -06:00
Victor Grau Serrat 7eb184d946 exchange unit tests 2017-10-24 17:44:18 -06:00
fredfortier 1cc34a1485 Fixed urllib package for back compatibility 2017-10-24 19:01:30 -04:00
fredfortier aa2f2f3627 Filtered out starting dates before the calendar 2017-10-24 18:26:44 -04:00
fredfortier 7e373e2f9c Removing symbols.json in clean-exchange. 2017-10-24 18:02:58 -04:00
fredfortier 942e6f263c Fixed an issue with the bar reader. 2017-10-24 16:10:33 -04:00
fredfortier 2e6d7d28ba Fixed an issue with the bar reader. 2017-10-24 16:00:56 -04:00
fredfortier 3a823ea457 Python3 adjustments 2017-10-24 15:47:15 -04:00
fredfortier 4daba6cfb4 Added unit test 2017-10-24 15:46:14 -04:00
Victor Grau Serrat fa018e2e0c more bcolz unit tests 2017-10-24 13:42:53 -06:00
Victor Grau Serrat 315d25f7c0 Merge branch 'develop' of github.com:enigmampc/catalyst into develop 2017-10-24 12:32:00 -06:00
fredfortier cc7ffada96 Merge remote-tracking branch 'origin/develop' into develop 2017-10-24 14:23:57 -04:00
fredfortier 5394c1bc91 Fixed an issue with asset date in chunks 2017-10-24 14:23:47 -04:00
Victor Grau Serrat b230b73829 unit test bcolz writer 2017-10-24 11:28:31 -06:00
Victor Grau Serrat 930a68ab4a unit test for Bcolz writer expanded 2017-10-24 10:36:15 -06:00
Victor Grau Serrat 4e833981e4 unit test for Bcolz writer expanded 2017-10-24 09:55:37 -06:00
fredfortier 2ea402ff10 Modified bcolz unit test 2017-10-24 11:39:17 -04:00
Victor Grau Serrat da6b024edc unit test for Bcolz writer 2017-10-24 09:32:24 -06:00
Victor Grau Serrat 565e9a3cea Added param checking and help msg to clean bundle folders 2017-10-23 21:30:48 -06:00
fredfortier 3c10d19a7e Added method to clean bundle folders 2017-10-23 20:53:25 -04:00
fredfortier cf96e047cd Added method to clean bundle folders 2017-10-23 20:49:40 -04:00
fredfortier 6f6a8e1272 Merge remote-tracking branch 'origin/develop' into develop 2017-10-23 20:29:57 -04:00
fredfortier c2a02e7074 Fixed hash method to create sid numbers 2017-10-23 20:29:48 -04:00
Victor Grau Serrat 7d2cf97fbf FIX: Conda install for Windows 2017-10-23 16:02:28 -06:00
Victor Grau Serrat 195469897c FIX: Windows path 2017-10-23 14:43:56 -06:00
fredfortier c7b422d465 Fix to work around empty bundles 2017-10-22 18:14:35 -04:00
Victor Grau Serrat 2dbace37bb Merge branch 'develop' - Release 0.3.1
FIX: bundle start_dt cannot be earlier than asset_start
FIX: prior raise of AuthNotFound, now generates empty auth.json, and raises AuthEmpty when live
FIX: os.path.join to make BUNDLE_NAME_TEMPLATE compatible across OSes
2017-10-21 22:57:47 -06:00
Victor Grau Serrat 2e903fd42c FIX: bundle start_dt, empty auth, bundle_name_template->os.path.join 2017-10-21 22:56:22 -06:00
reinka 47a104b29c [MIG] Migrated to version 0.3 to work with Poloniex exchange. 2017-10-21 11:26:34 +02:00
fredfortier d248581523 Fixed an error message 2017-10-21 00:27:05 -04:00
fredfortier 48f6300e08 Optimized imports 2017-10-20 23:18:15 -04:00
VictorandGitHub f7a143cb78 Merge pull request #41 from abnera/patch-1
Fix issues with .yml file and incompatible packages.
2017-10-20 15:46:02 -06:00
Victor Grau Serrat 2f7cd97852 DOC: WIP fix tutorial 2017-10-20 15:37:04 -06:00
Abner Ayala-AcevedoandGitHub 73eca75ed9 Updated conda .yml file to work with enigma 0.3 or above.
Removed unnecessary libraries that were giving issues.
2017-10-20 14:30:06 -07:00
Victor Grau Serrat 2ade2989e8 Merge branch 'develop' -> release 0.3 2017-10-20 14:53:23 -06:00
Victor Grau Serrat b1d5acf2ad DOC: jupyter notebook in beginner tutorial 2017-10-20 14:51:01 -06:00
Victor Grau Serrat 5d5ec6b9be DOC: jupyter notebook in beginner tutorial 2017-10-20 14:49:54 -06:00
Victor Grau Serrat 1b84023c5d Merge branch 'concurrent-exchanges' into develop 2017-10-20 13:42:26 -06:00
Victor Grau Serrat 97f3329c1b centralizing LOG_LEVEL 2017-10-20 13:41:33 -06:00
fredfortier 493fc95a20 Fixed an issue with historical data in live mode 2017-10-20 15:17:29 -04:00
Victor Grau Serrat bdeb344999 constants.py, WIP: system-wide log level 2017-10-20 13:08:55 -06:00
Victor Grau Serrat 52e1de954f Resolving conflicts between branches 2017-10-20 12:15:58 -06:00
Victor Grau Serrat 7b9eafef4e Merge branch 'master' into develop 2017-10-20 12:09:51 -06:00
fredfortier f918fc97bc Fix an issue with data.history() in backtest mode 2017-10-20 13:36:39 -04:00
fredfortier 18e19bb1ae Merge remote-tracking branch 'origin/concurrent-exchanges' into concurrent-exchanges 2017-10-20 13:17:10 -04:00
fredfortier f72074876d Misc small fixes 2017-10-20 13:17:02 -04:00
Victor Grau Serrat fadd4abe5a DOC: naming convention 2017-10-20 10:55:35 -06:00
Victor Grau Serrat 5fd4ca33d3 DOC: beginner tutorial 2017-10-20 10:14:31 -06:00
Victor Grau Serrat 653f4c2a5a DOC: Features 2017-10-20 08:27:36 -06:00
Victor Grau Serrat 3804af3813 DOC: welcome page w/ logo 2017-10-20 00:13:23 -06:00
Victor Grau Serrat f56abcfc3e DOC: welcome page 2017-10-19 23:54:02 -06:00
Victor Grau Serrat cb6432c395 docs: Catalyst Install 2017-10-19 23:32:55 -06:00
fredfortier 946d24bd7a Refactoring related to auto-ingestion 2017-10-19 23:23:37 -04:00
Victor Grau Serrat b1a247df6a gh-pages initial build: Installation (WIP) 2017-10-19 18:03:13 -06:00
Victor Grau Serrat 2c91decc1b WIP: docs build 2017-10-19 15:31:43 -06:00
Victor Grau Serrat 09f27e5880 WIP: build docs 2017-10-19 14:45:32 -06:00
Victor Grau Serrat 2dd8f54148 WIP: build docs 2017-10-19 14:35:25 -06:00
fredfortier 2d41f124f0 Merge remote-tracking branch 'origin/concurrent-exchanges' into concurrent-exchanges 2017-10-19 15:24:08 -04:00
fredfortier 619eb3cfa4 Fixed an issue with data.history more recent than the server 2017-10-19 15:24:00 -04:00
Victor Grau Serrat 331a31b25a WIP: docs build 2017-10-19 13:18:21 -06:00
Victor Grau Serrat 6f57660944 WIP: docs build 2017-10-19 12:55:42 -06:00
fredfortier 1a97111ceb Merge remote-tracking branch 'origin/concurrent-exchanges' into concurrent-exchanges 2017-10-19 14:49:41 -04:00
fredfortier 2502c9a2bb Minor fixes 2017-10-19 14:49:32 -04:00
Victor Grau Serrat 675957b197 Open calendar starts on 2015-02-19 2017-10-19 10:04:17 -06:00
fredfortier 51172759d3 Fixed some issues and optimized data.history() in live mode 2017-10-19 05:19:01 -04:00
fredfortier 6097128d5c Fixed small issue with minute ingestion 2017-10-18 23:45:42 -04:00
fredfortier 5ccdd54274 Merge remote-tracking branch 'origin/concurrent-exchanges' into concurrent-exchanges 2017-10-18 23:26:31 -04:00
fredfortier b3dcb7a9ad Fixed issue with overlapping chunks 2017-10-18 23:26:24 -04:00
Victor Grau Serrat 8a6d0d7ca0 Catch NoData on Exchange + formatting of errors 2017-10-18 21:25:27 -06:00
fredfortier e5f7c63ebd Fixed an issue with minute bundles 2017-10-18 20:43:26 -04:00
fredfortier 2e46323a9e Fixed an issue with minute bundles 2017-10-18 20:30:24 -04:00
fredfortier 874a4bb682 Fixed an issue with reader array size 2017-10-18 18:33:37 -04:00
fredfortier 339fa21c35 Fixed an issue with the backtest get_history_window method. 2017-10-18 17:23:33 -04:00
fredfortier 1c5822bce9 Merge remote-tracking branch 'origin/concurrent-exchanges' into concurrent-exchanges 2017-10-18 16:41:37 -04:00
fredfortier b785c10036 Fixed misc issues with the bundle refactoring 2017-10-18 16:41:29 -04:00
Victor Grau Serrat b69e78b27d fixes ingestion of 'minute,daily' parameter 2017-10-18 14:10:03 -06:00
Victor Grau Serrat 6486744c66 fix exchange_bundle: period month padded 2017-10-18 13:55:39 -06:00
fredfortier 6f8fbc2b82 Fix issue with retrieving bundles 2017-10-18 15:33:15 -04:00
fredfortier 521484355a Minor fix to the bcolz writer 2017-10-18 14:45:56 -04:00
fredfortier 7147bfc51f Refactoring to use the updated bundles 2017-10-18 14:36:25 -04:00
fredfortier b357a0656a Added a modified bcolz writer / reader 2017-10-18 13:58:37 -04:00
fredfortier 86f892eade Unit tested a daily reader/writer based on the minute bundle 2017-10-18 04:29:22 -04:00
fredfortier 188a4a3f3d Unit testing an issue with the daily loader 2017-10-18 02:11:21 -04:00
Victor Grau Serrat fb32e1ce5d Fixing DailyBarReader for volume to 0 instead of NaN 2017-10-17 23:16:49 -06:00
fredfortier 733f2c3433 Fixed an issue with writer retry 2017-10-18 00:18:07 -04:00
fredfortier 74fd4a6a0f Trying to fix an issue with merging new candles in get_history() 2017-10-18 00:04:55 -04:00
fredfortier 1a4dfe8abb Fixed date range issues and issues retrieving the benchmark data 2017-10-17 21:01:00 -04:00
fredfortier 6c17bbf0c9 Merge remote-tracking branch 'origin/concurrent-exchanges' into concurrent-exchanges
# Conflicts:
#	catalyst/exchange/exchange_bundle.py
2017-10-17 18:31:38 -04:00
fredfortier 4649b31d89 Fixed issues with daily bundles 2017-10-17 18:29:48 -04:00
Victor Grau Serrat ead6769ea2 retrieve benchmark from ExchangeBundle 2017-10-17 15:39:20 -06:00
fredfortier d21cc36bef Fixes a start date issue 2017-10-17 16:49:04 -04:00
fredfortier 105fee0fb9 Fixes to the daily data 2017-10-17 15:35:49 -04:00
Victor Grau Serrat a4389ffea4 download symbols.json when older than 1 day 2017-10-17 10:17:35 -06:00
fredfortier 989ffc57f1 Merge remote-tracking branch 'origin/concurrent-exchanges' into concurrent-exchanges 2017-10-17 03:00:45 -04:00
fredfortier 9bdd8aba48 Implemented daily data loader and related fixes 2017-10-17 03:00:36 -04:00
Victor Grau Serrat dadf7bd108 Merge branch 'concurrent-exchanges' of github.com:enigmampc/catalyst into concurrent-exchanges 2017-10-16 22:35:57 -06:00
Victor Grau Serrat e98d10c41b Fix floats for volume in data.history 2017-10-16 22:29:33 -06:00
fredfortier 1263fdd995 Testing related adjustments 2017-10-16 15:38:07 -04:00
fredfortier 1732b4a985 Testing related adjustments 2017-10-16 03:09:13 -04:00
fredfortier 403f951c77 Added unit tests 2017-10-15 05:11:44 -04:00
fredfortier bdbaad1c91 Improvements and fixes to the ingestion component 2017-10-14 02:06:26 -04:00
fredfortier c52653c84e Tested ingestion of minute data with a single market 2017-10-13 21:00:47 -04:00
fredfortier 93f4d31399 Unit tested ingestion of bundle chunks. This may not be stable yet. 2017-10-13 16:29:43 -04:00
fredfortier c658d15fcb Unit testing ingestion of bundles logic 2017-10-13 00:50:25 -04:00
fredfortier e1c2f40ab9 Making some adjustments to the ingestion method after discussion with Victor 2017-10-12 14:06:47 -04:00
Victor Grau Serrat 1a87d5a0c0 Making errors more verbose and user-friendly 2017-10-12 09:15:45 -06:00
Victor Grau Serrat 1dcfd169fa FIX: Raising Exceptions without traceback 2017-10-11 23:40:06 -06:00
Victor Grau Serrat 1c7fd19652 FIX: Raising Exceptions without traceback 2017-10-11 23:33:12 -06:00
Victor Grau Serrat c67cbedfbf FIX: Raising Exceptions without traceback 2017-10-11 23:31:08 -06:00
fredfortier 73378962aa Bug fixes and housekeeping from ingestion testing 2017-10-12 01:24:21 -04:00
fredfortier 4895bef392 Bug fixes and housekeeping from ingestion testing 2017-10-12 00:51:18 -04:00
fredfortier 3f9b44f3e4 Merge remote-tracking branch 'origin/concurrent-exchanges' into concurrent-exchanges 2017-10-11 22:05:37 -04:00
fredfortier c24918e2c8 Bug fixes 2017-10-11 22:05:29 -04:00
Victor Grau Serrat 01aeb88e8f Raising Exceptions without traceback 2017-10-11 17:05:27 -06:00
fredfortier c33bab673f Merge remote-tracking branch 'origin/concurrent-exchanges' into concurrent-exchanges 2017-10-11 00:13:34 -04:00
fredfortier d3e33c44bf Reading data from bundles first and other fixes 2017-10-11 00:13:22 -04:00
Victor Grau Serrat de409efd3e API uses Catalyst naming convention 2017-10-10 14:22:49 -06:00
fredfortier 83af12c52c Added exchange get_history method which merge historical bars from the Catalyst and exchange APIs 2017-10-09 16:23:02 -04:00
fredfortier 8811aa669a Naive integration with the consolidated exchanges api (minor fix) 2017-10-09 14:52:59 -04:00
fredfortier 4f80ebee57 Naive integration with the consolidated exchanges api 2017-10-09 14:50:51 -04:00
fredfortier 403be97143 Integrating with history api 2017-10-08 02:27:13 -04:00
fredfortier 16cdc196b0 Minor fixes after merging 2017-10-08 01:18:40 -04:00
fredfortier 1d79e88312 Merge remote-tracking branch 'origin/concurrent-exchanges' into concurrent-exchanges
# Conflicts:
#	catalyst/exchange/bundle_utils.py
2017-10-08 01:15:47 -04:00
fredfortier 3335ae0ea9 Refactored the data portal to use the exchange bundles 2017-10-08 01:13:47 -04:00
Victor Grau Serrat a1cf00e6fe updated symbols.json for 3 exchanges: end_daily, end_minute 2017-10-06 21:03:31 -06:00
fredfortier 0cc9d839d0 Optimize the existing data filter to filter by asset. 2017-10-06 15:13:40 -04:00
fredfortier a004a01cdb Skipping data chunks if they already exist (fix) 2017-10-06 14:33:05 -04:00
fredfortier 04fc7855d5 Skipping data chunks if they already exist 2017-10-06 14:29:25 -04:00
fredfortier 50f075792c Tested ingestion after refactoring 2017-10-05 21:03:39 -04:00
Victor Grau Serrat 14f8c25c89 get_history against AWS API 2017-10-05 17:28:19 -06:00
fredfortier 874968bbbb Refactoring the exchange bundle for incremental loading 2017-10-05 18:06:17 -04:00
fredfortier 751608c8ab Mocking Victor's history API service 2017-10-04 22:35:07 -04:00
Victor Grau Serrat e11ecf9d78 Added 'live' mode to CLI instead of option to 'run' 2017-09-29 13:38:58 -06:00
Victor Grau Serrat b45339692f poloniex autogeneration of symbols.json with optional sourcing of start_date 2017-09-28 16:14:27 -06:00
Victor Grau Serrat 336f062794 poloniex autogeneration of symbols.json with cached start_date 2017-09-28 14:42:47 -06:00
Victor Grau Serrat 6d8b8307a1 bitfinex autogeneration of symbols.json with optional sourcing of start_date 2017-09-28 14:14:34 -06:00
Victor Grau Serrat 3b681d197d Purge 5-min implementation 2017-09-28 11:03:47 -06:00
Victor Grau Serrat 15fa98420d Catching bitfinex Error: No JSON object could be decoded 2017-09-28 09:14:22 -06:00
fredfortier 9dfefec13c Merge branch 'concurrent-exchanges' of github.com:enigmampc/catalyst into concurrent-exchanges 2017-09-27 17:31:44 -04:00
fredfortier 6bfe0eecd2 Remove some 5-minute data and added example of extension.py. 2017-09-27 17:27:40 -04:00
Victor Grau Serrat 3362dbf95c WIP: Poloniex exchange - placing orders, executing transactions 2017-09-27 14:31:15 -06:00
Victor Grau Serrat 1d0faf693d WIP: Poloniex exchange - create order 2017-09-26 15:36:14 -06:00
Victor Grau Serrat 87ecf6114d adding min_trade_size in TradingPair 2017-09-26 13:32:23 -06:00
Victor Grau Serrat 2c2c861a8f WIP: Poloniex exchange - fix for multiple exchanges 2017-09-26 11:40:25 -06:00
Victor Grau Serrat fef08d1433 Merge branch 'poloniex-exchange' into concurrent-exchanges 2017-09-26 10:53:56 -06:00
Victor Grau Serrat cf20f78e55 WIP: Poloniex exchange - balances, candles & cancel 2017-09-25 22:01:04 -06:00
Victor Grau Serrat 5d1bdee4a6 WIP: Poloniex exchange - generating symbols.json 2017-09-25 14:35:58 -06:00
Victor Grau Serrat f60abcd636 WIP: Poloniex exchange class 2017-09-25 11:28:06 -06:00
fredfortier 4798fc75fb Housekeeping and documentation 2017-09-25 12:18:22 -04:00
fredfortier d6996b1e93 Refinements and documentation. 2017-09-23 04:49:13 -04:00
fredfortier bc65c10fc6 Implemented and tested the history() method in backtest mode. 2017-09-22 23:17:38 -04:00
Victor Grau Serrat 27f20a090a matplotlib imports inside init live_graph_clock (2) 2017-09-22 12:03:17 -06:00
Victor Grau Serrat 75c2753b98 matplotlib imports inside init live_graph_clock 2017-09-22 11:59:57 -06:00
Victor Grau Serrat a6b873508b Merge branch 'aws-symbols-json' into develop 2017-09-22 10:57:58 -06:00
Victor Grau Serrat daf3c4d285 Autogeneration of symbols.json for bittrex 2017-09-22 10:55:49 -06:00
Victor Grau Serrat 8cabb33372 Autogeneration of symbols.json for bitfinex 2017-09-22 09:54:51 -06:00
fredfortier ddecd6bb48 First working version with the backtest and live modes executing the same algorithm. 2017-09-21 19:05:16 -04:00
fredfortier 2f8768bb06 Merged Victor's hack for the minute writer precision 2017-09-21 16:36:10 -04:00
Victor Grau Serrat df8ba90236 {exchange}/symbols.json moved to AWS 2017-09-21 12:43:29 -06:00
Victor Grau Serrat 6baf4c2122 Merge branch 'master' into develop 2017-09-20 23:40:04 -06:00
fredfortier 7335810cc2 Defined the same commission model as with equities for now. We need to fix the data precision in the bundles. 2017-09-21 01:17:10 -04:00
fredfortier 10a5b5412e Testing the same algo in live and backtest mode. Most of it works well. We need a commission model for the TradingPair currency type. 2017-09-20 23:48:57 -04:00
Victor Grau Serrat c5cbbce8e1 Merge branch 'master' into develop 2017-09-20 16:23:21 -06:00
fredfortier 4e2d092123 Trying to fix an issue with periodical bars 2017-09-20 18:00:08 -04:00
fredfortier 3b655d466e Unit tested exchange loader extension and backtest data portal refactoring 2017-09-20 05:11:54 -04:00
fredfortier 68546a0d8d Experimenting with simpler bundle and data portal approach (works in unit testing) 2017-09-19 03:49:34 -04:00
fredfortier b70ff3a740 Bug fixes and working on unit tests for the data portal 2017-09-18 22:19:27 -04:00
fredfortier 18bfaff7c9 Trying to stabilize refactoring an last few commits (still unstable) 2017-09-18 15:37:10 -04:00
fredfortier 555b7e95b5 Working on adjusted the DataPortal class (unstable) 2017-09-18 14:51:01 -04:00
fredfortier 1d6336afda Splitting the exchange_algorithm class to allow access to the symbol() method in backtesting mode 2017-09-18 14:48:24 -04:00
fredfortier 394777217d Splitting the exchange_algorithm class to allow access to the symbol() method in backtesting mode 2017-09-18 13:56:30 -04:00
fredfortier 5a345a3abb Documentation and cleanup from meeting with Victor 2017-09-15 18:00:15 -04:00
fredfortier ff0dc5cff9 Polishing the sample arbitrage algo 2017-09-12 14:25:04 -04:00
fredfortier 41d9bbca1b Adjustments to the sample arbitrage algo 2017-09-11 18:20:28 -04:00
fredfortier 3e2a8dd78b Adjustments to the sample arbitrage algo 2017-09-11 18:03:58 -04:00
fredfortier 7e280aeb5c Working on multiple exchanges and a sample algo for arbitrage 2017-09-10 20:20:34 -04:00
fredfortier 36881b03e2 Working on multi-exchange implementation (not fully tested) 2017-09-07 23:54:11 -04:00
116 changed files with 28155 additions and 3796 deletions
+3 -1
View File
@@ -1 +1,3 @@
All the documentation for `Catalyst <https://github.com/enigmampc/catalyst>`_ can be found in the `catalyst-docs wiki <https://github.com/enigmampc/catalyst-docs/wiki>`_. All the documentation for `Catalyst <https://github.com/enigmampc/catalyst>`_
can be found in the
`documentation website <https://enigmampc.github.io/catalyst>`_.
+335 -59
View File
@@ -8,6 +8,9 @@ import pandas as pd
from six import text_type 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_utils import delete_algo_folder
from catalyst.exchange.factory import get_exchange
from catalyst.utils.cli import Date, Timestamp from catalyst.utils.cli import Date, Timestamp
from catalyst.utils.run_algo import _run, load_extensions from catalyst.utils.run_algo import _run, load_extensions
@@ -27,17 +30,19 @@ 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 $ZIPLINE_HOME.", help="Don't load the default catalyst extension.py file "
"in $CATALYST_HOME.",
) )
@click.version_option()
def main(extension, strict_extensions, default_extension): def main(extension, strict_extensions, default_extension):
"""Top level catalyst entry point. """Top level catalyst entry point.
""" """
@@ -120,13 +125,13 @@ 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',
type=click.Choice({'daily', '5-minute', 'minute'}), type=click.Choice({'daily', 'minute'}),
default='daily', default='daily',
show_default=True, show_default=True,
help='The data frequency of the simulation.', help='The data frequency of the simulation.',
@@ -134,7 +139,6 @@ def ipython_only(option):
@click.option( @click.option(
'--capital-base', '--capital-base',
type=float, type=float,
default=10e6,
show_default=True, show_default=True,
help='The starting capital for the simulation.', help='The starting capital for the simulation.',
) )
@@ -172,8 +176,8 @@ def ipython_only(option):
default='-', default='-',
metavar='FILENAME', metavar='FILENAME',
show_default=True, show_default=True,
help="The location to write the perf data. If this is '-' the perf will" help="The location to write the perf data. If this is '-' the perf"
" be written to stdout.", " will be written to stdout.",
) )
@click.option( @click.option(
'--print-algo/--no-print-algo', '--print-algo/--no-print-algo',
@@ -187,17 +191,12 @@ def ipython_only(option):
default=None, default=None,
help='Should the algorithm methods be resolved in the local namespace.' help='Should the algorithm methods be resolved in the local namespace.'
)) ))
@click.option(
'--live/--no-live',
is_flag=True,
default=False,
help='Enable live trading.',
)
@click.option( @click.option(
'-x', '-x',
'--exchange-name', '--exchange-name',
type=click.Choice({'bitfinex', 'bittrex'}), type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
help='The name of the targeted exchange (supported: bitfinex, bittrex).', help='The name of the targeted exchange (supported: bitfinex,'
' bittrex, poloniex).',
) )
@click.option( @click.option(
'-n', '-n',
@@ -210,12 +209,6 @@ def ipython_only(option):
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(
'--live-graph/--no-live-graph',
is_flag=True,
default=False,
help='Display live graph.',
)
@click.pass_context @click.pass_context
def run(ctx, def run(ctx,
algofile, algofile,
@@ -230,44 +223,44 @@ def run(ctx,
output, output,
print_algo, print_algo,
local_namespace, local_namespace,
live,
exchange_name, exchange_name,
algo_namespace, algo_namespace,
base_currency, base_currency):
live_graph):
"""Run a backtest for the given algorithm. """Run a backtest for the given algorithm.
""" """
if live:
if exchange_name is None:
ctx.fail("must specify an exchange name '-x' in live execution "
"mode '--live'")
if algo_namespace is None:
ctx.fail("must specify an algorithm name '-n' in live execution "
"mode '--live'")
if base_currency is None:
ctx.fail("must specify a base currency '-c' in live "
"execution mode '--live'")
else:
# check that the start and end dates are passed correctly
if start is None and end is None:
# check both at the same time to avoid the case where a user
# does not pass either of these and then passes the first only
# to be told they need to pass the second argument also
ctx.fail(
"must specify dates with '-s' / '--start' and '-e' / '--end'",
)
if start is None:
ctx.fail("must specify a start date with '-s' / '--start'")
if end is None:
ctx.fail("must specify an end date with '-e' / '--end'")
if (algotext is not None) == (algofile is not None): if (algotext is not None) == (algofile is not None):
ctx.fail( ctx.fail(
"must specify exactly one of '-f' / '--algofile' or" "must specify exactly one of '-f' / '--algofile' or"
" '-t' / '--algotext'", " '-t' / '--algotext'",
) )
# check that the start and end dates are passed correctly
if start is None and end is None:
# check both at the same time to avoid the case where a user
# does not pass either of these and then passes the first only
# to be told they need to pass the second argument also
ctx.fail(
"must specify dates with '-s' / '--start' and '-e' / '--end'"
" in backtest mode",
)
if start is None:
ctx.fail("must specify a start date with '-s' / '--start'"
" in backtest mode")
if end is None:
ctx.fail("must specify an end date with '-e' / '--end'"
" in backtest mode")
if exchange_name is None:
ctx.fail("must specify an exchange name '-x'")
if base_currency is None:
ctx.fail("must specify a base currency with '-c' in backtest mode")
if capital_base is None:
ctx.fail("must specify a capital base with '--capital-base'"
" in backtest mode")
perf = _run( perf = _run(
initialize=None, initialize=None,
handle_data=None, handle_data=None,
@@ -287,11 +280,11 @@ def run(ctx,
print_algo=print_algo, print_algo=print_algo,
local_namespace=local_namespace, local_namespace=local_namespace,
environ=os.environ, environ=os.environ,
live=live, live=False,
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=False
) )
if output == '-': if output == '-':
@@ -335,15 +328,288 @@ def catalyst_magic(line, cell=None):
raise ValueError('main returned non-zero status code: %d' % e.code) raise ValueError('main returned non-zero status code: %d' % e.code)
@main.command()
@click.option(
'-f',
'--algofile',
default=None,
type=click.File('r'),
help='The file that contains the algorithm to run.',
)
@click.option(
'-t',
'--algotext',
help='The algorithm script to run.',
)
@click.option(
'-D',
'--define',
multiple=True,
help="Define a name to be bound in the namespace before executing"
" the algotext. For example '-Dname=value'. The value may be"
" any python expression. These are evaluated in order so they"
" may refer to previously defined names.",
)
@click.option(
'-o',
'--output',
default='-',
metavar='FILENAME',
show_default=True,
help="The location to write the perf data. If this is '-' the perf will"
" be written to stdout.",
)
@click.option(
'--print-algo/--no-print-algo',
is_flag=True,
default=False,
help='Print the algorithm to stdout.',
)
@ipython_only(click.option(
'--local-namespace/--no-local-namespace',
is_flag=True,
default=None,
help='Should the algorithm methods be resolved in the local namespace.'
))
@click.option(
'-x',
'--exchange-name',
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
help='The name of the targeted exchange (supported: bitfinex,'
' bittrex, poloniex).',
)
@click.option(
'-n',
'--algo-namespace',
help='A label assigned to the algorithm for data storage purposes.'
)
@click.option(
'-c',
'--base-currency',
help='The base currency used to calculate statistics '
'(e.g. usd, btc, eth).',
)
@click.option(
'--live-graph/--no-live-graph',
is_flag=True,
default=False,
help='Display live graph.',
)
@click.pass_context
def live(ctx,
algofile,
algotext,
define,
output,
print_algo,
local_namespace,
exchange_name,
algo_namespace,
base_currency,
live_graph):
"""Trade live with the given algorithm.
"""
if (algotext is not None) == (algofile is not None):
ctx.fail(
"must specify exactly one of '-f' / '--algofile' or"
" '-t' / '--algotext'",
)
if exchange_name is None:
ctx.fail("must specify an exchange name '-x'")
if algo_namespace is None:
ctx.fail("must specify an algorithm name '-n' in live execution mode")
if base_currency is None:
ctx.fail("must specify a base currency '-c' in live execution mode")
perf = _run(
initialize=None,
handle_data=None,
before_trading_start=None,
analyze=None,
algofile=algofile,
algotext=algotext,
defines=define,
data_frequency=None,
capital_base=None,
data=None,
bundle=None,
bundle_timestamp=None,
start=None,
end=None,
output=output,
print_algo=print_algo,
local_namespace=local_namespace,
environ=os.environ,
live=True,
exchange=exchange_name,
algo_namespace=algo_namespace,
base_currency=base_currency,
live_graph=live_graph
)
if output == '-':
click.echo(str(perf))
elif output != os.devnull: # make the catalyst magic not write any data
perf.to_pickle(output)
return perf
@main.command(name='ingest-exchange')
@click.option(
'-x',
'--exchange-name',
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
help='The name of the exchange bundle to ingest (supported: bitfinex,'
' bittrex, poloniex).',
)
@click.option(
'-f',
'--data-frequency',
type=click.Choice({'daily', 'minute', 'daily,minute', 'minute,daily'}),
default='daily',
show_default=True,
help='The data frequency of the desired OHLCV bars.',
)
@click.option(
'-s',
'--start',
default=None,
type=Date(tz='utc', as_timestamp=True),
help='The start date of the data range. (default: one year from end date)',
)
@click.option(
'-e',
'--end',
default=None,
type=Date(tz='utc', as_timestamp=True),
help='The end date of the data range. (default: today)',
)
@click.option(
'-i',
'--include-symbols',
default=None,
help='A list of symbols to ingest (optional comma separated list)',
)
@click.option(
'--exclude-symbols',
default=None,
help='A list of symbols to exclude from the ingestion '
'(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(
'--show-progress/--no-show-progress',
default=True,
help='Print progress information to the terminal.'
)
@click.option(
'--verbose/--no-verbose`',
default=False,
help='Show a progress indicator for every currency pair.'
)
@click.option(
'--validate/--no-validate`',
default=False,
help='Report potential anomalies found in data bundles.'
)
def ingest_exchange(exchange_name, data_frequency, start, end,
include_symbols, exclude_symbols, csv, show_progress,
verbose, validate):
"""
Ingest data for the given exchange.
"""
if exchange_name is None:
ctx.fail("must specify an exchange name '-x'")
exchange_bundle = ExchangeBundle(exchange_name)
click.echo('Ingesting exchange bundle {}...'.format(exchange_name))
exchange_bundle.ingest(
data_frequency=data_frequency,
include_symbols=include_symbols,
exclude_symbols=exclude_symbols,
start=start,
end=end,
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')
@click.option(
'-x',
'--exchange-name',
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
help='The name of the exchange bundle to ingest (supported: bitfinex,'
' bittrex, poloniex).',
)
@click.option(
'-f',
'--data-frequency',
type=click.Choice({'daily', 'minute'}),
default=None,
help='The bundle data frequency to remove. If not specified, it will '
'remove both daily and minute bundles.',
)
@click.pass_context
def clean_exchange(ctx, exchange_name, data_frequency):
"""Clean up bundles from 'ingest-exchange'.
"""
if exchange_name is None:
ctx.fail("must specify an exchange name '-x'")
exchange_bundle = ExchangeBundle(exchange_name)
click.echo('Cleaning exchange bundle {}...'.format(exchange_name))
exchange_bundle.clean(
data_frequency=data_frequency,
)
click.echo('Done')
@main.command() @main.command()
@click.option( @click.option(
'-b', '-b',
'--bundle', '--bundle',
default='poloniex',
metavar='BUNDLE-NAME', metavar='BUNDLE-NAME',
show_default=True, default=None,
show_default=False,
help='The data bundle to ingest.', help='The data bundle to ingest.',
) )
@click.option(
'-x',
'--exchange-name',
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
help='The name of the exchange bundle to ingest (supported: bitfinex,'
' bittrex, poloniex).',
)
@click.option( @click.option(
'-c', '-c',
'--compile-locally', '--compile-locally',
@@ -362,9 +628,12 @@ def catalyst_magic(line, cell=None):
default=True, default=True,
help='Print progress information to the terminal.' help='Print progress information to the terminal.'
) )
def ingest(bundle, compile_locally, assets_version, show_progress): @click.pass_context
def ingest(ctx, bundle, exchange_name, compile_locally, assets_version,
show_progress):
"""Ingest the data for the given bundle. """Ingest the data for the given bundle.
""" """
bundles_module.ingest( bundles_module.ingest(
bundle, bundle,
os.environ, os.environ,
@@ -384,6 +653,13 @@ def ingest(bundle, compile_locally, assets_version, show_progress):
show_default=True, show_default=True,
help='The data bundle to clean.', help='The data bundle to clean.',
) )
@click.option(
'-x',
'--exchange_name',
metavar='EXCHANGE-NAME',
show_default=True,
help='The exchange bundle name to clean.',
)
@click.option( @click.option(
'-e', '-e',
'--before', '--before',
@@ -407,7 +683,7 @@ def ingest(bundle, compile_locally, assets_version, show_progress):
' This may not be passed with -e / --before or -a / --after', ' This may not be passed with -e / --before or -a / --after',
) )
def clean(bundle, before, after, keep_last): def clean(bundle, before, after, keep_last):
"""Clean up data downloaded with the ingest command. """Clean up bundles from 'ingest'.
""" """
bundles_module.clean( bundles_module.clean(
bundle, bundle,
+14 -39
View File
@@ -134,15 +134,13 @@ from catalyst.utils.security_list import SecurityList
import catalyst.protocol import catalyst.protocol
from catalyst.sources.requests_csv import PandasRequestsCSV from catalyst.sources.requests_csv import PandasRequestsCSV
from catalyst.gens.sim_engine import ( from catalyst.gens.sim_engine import MinuteSimulationClock
MinuteSimulationClock,
FiveMinuteSimulationClock,
)
from catalyst.sources.benchmark_source import BenchmarkSource from catalyst.sources.benchmark_source import BenchmarkSource
from catalyst.catalyst_warnings import ZiplineDeprecationWarning from catalyst.catalyst_warnings import ZiplineDeprecationWarning
from catalyst.constants import LOG_LEVEL
log = logbook.Logger("ZiplineLog") log = logbook.Logger("CatalystLog", level=LOG_LEVEL)
class TradingAlgorithm(object): class TradingAlgorithm(object):
@@ -174,7 +172,7 @@ class TradingAlgorithm(object):
algo_filename : str, optional algo_filename : str, optional
The filename for the algoscript. This will be used in exception The filename for the algoscript. This will be used in exception
tracebacks. default: '<string>'. tracebacks. default: '<string>'.
data_frequency : {'daily', '5-minute', 'minute'}, optional data_frequency : {'daily', 'minute'}, optional
The duration of the bars. The duration of the bars.
instant_fill : bool, optional instant_fill : bool, optional
Whether to fill orders immediately or on next bar. default: False Whether to fill orders immediately or on next bar. default: False
@@ -227,7 +225,7 @@ class TradingAlgorithm(object):
script : str script : str
Algoscript that contains initialize and Algoscript that contains initialize and
handle_data function definition. handle_data function definition.
data_frequency : {'daily', '5-minute', 'minute'} data_frequency : {'daily', 'minute'}
The duration of the bars. The duration of the bars.
capital_base : float <default: 1.0e5> capital_base : float <default: 1.0e5>
How much capital to start with. How much capital to start with.
@@ -435,8 +433,6 @@ class TradingAlgorithm(object):
if get_loader is not None: if get_loader is not None:
if data_frequency == 'daily': if data_frequency == 'daily':
all_dates = self.trading_calendar.all_sessions all_dates = self.trading_calendar.all_sessions
elif data_frequency == '5-minute':
all_dates = self.trading_calendar.all_five_minutes
elif data_frequency == 'minute': elif data_frequency == 'minute':
all_dates = self.trading_calendar.all_minutes all_dates = self.trading_calendar.all_minutes
else: else:
@@ -468,7 +464,7 @@ class TradingAlgorithm(object):
self._in_before_trading_start = True self._in_before_trading_start = True
with handle_non_market_minutes(data) if \ with handle_non_market_minutes(data) if \
self.data_frequency in ('minute', '5-minute') else ExitStack(): self.data_frequency == 'minute' else ExitStack():
self._before_trading_start(self, data) self._before_trading_start(self, data)
self._in_before_trading_start = False self._in_before_trading_start = False
@@ -524,11 +520,10 @@ class TradingAlgorithm(object):
market_closes = trading_o_and_c['market_close'] market_closes = trading_o_and_c['market_close']
minutely_emission = False minutely_emission = False
if self.sim_params.data_frequency in set(('minute', '5-minute')): if self.sim_params.data_frequency == 'minute':
market_opens = trading_o_and_c['market_open'] market_opens = trading_o_and_c['market_open']
minutely_emission = self.sim_params.emission_rate in \ minutely_emission = self.sim_params.emission_rate == 'minute'
set(('minute', '5-minute'))
else: else:
# in daily mode, we want to have one bar per session, timestamped # in daily mode, we want to have one bar per session, timestamped
# as the last minute of the session. # as the last minute of the session.
@@ -552,15 +547,6 @@ class TradingAlgorithm(object):
'UTC', 'UTC',
) )
if self.sim_params.data_frequency == '5-minute':
return FiveMinuteSimulationClock(
self.sim_params.sessions,
execution_opens,
execution_closes,
before_trading_start_minutes,
minute_emission=minutely_emission,
)
return MinuteSimulationClock( return MinuteSimulationClock(
self.sim_params.sessions, self.sim_params.sessions,
execution_opens, execution_opens,
@@ -692,8 +678,6 @@ class TradingAlgorithm(object):
time_count = times.nunique() time_count = times.nunique()
if time_count == 1: if time_count == 1:
self.sim_params.data_frequency = 'daily' self.sim_params.data_frequency = 'daily'
elif time_count == 288:
self.sim_params.data_frequency = '5-minute'
else: else:
self.sim_params.data_frequency = 'minute' self.sim_params.data_frequency = 'minute'
@@ -715,8 +699,6 @@ class TradingAlgorithm(object):
if self.sim_params.data_frequency == 'daily': if self.sim_params.data_frequency == 'daily':
equity_reader_arg = 'equity_daily_reader' equity_reader_arg = 'equity_daily_reader'
elif self.sim_params.data_frequency == '5-minute':
equity_daily_reader = 'equity_5_minute_reader'
elif self.sim_params.data_frequency == 'minute': elif self.sim_params.data_frequency == 'minute':
equity_reader_arg = 'equity_minute_reader' equity_reader_arg = 'equity_minute_reader'
equity_reader = PanelBarReader( equity_reader = PanelBarReader(
@@ -960,9 +942,9 @@ class TradingAlgorithm(object):
The arena from the simulation parameters. This will normally The arena from the simulation parameters. This will normally
be ``'backtest'`` but some systems may use this distinguish be ``'backtest'`` but some systems may use this distinguish
live trading from backtesting. live trading from backtesting.
data_frequency : {'daily', '5-minute', 'minute'} data_frequency : {'daily', 'minute'}
data_frequency tells the algorithm if it is running with data_frequency tells the algorithm if it is running with
daily, minute, or five-minute mode. daily or minute mode.
start : datetime start : datetime
The start date for the simulation. The start date for the simulation.
end : datetime end : datetime
@@ -1137,17 +1119,10 @@ class TradingAlgorithm(object):
'time_rule= when calling schedule_function without ' 'time_rule= when calling schedule_function without '
'specifying a date_rule', stacklevel=3) 'specifying a date_rule', stacklevel=3)
freq = self.sim_params.data_frequency
date_rule = date_rule or date_rules.every_day() date_rule = date_rule or date_rules.every_day()
if freq is 'daily': time_rule = ((time_rule or time_rules.every_minute())
# ignore time rule in daily mode if self.sim_params.data_frequency == 'minute' else
time_rule = time_rules.every_minute() # If we are in daily mode the time_rule is ignored.
else:
# use provided time rule or default to every minute or 5 minutes
# based on desired data frequency.
time_rule = time_rule or (time_rules.every_5_minutes()
if freq is '5-minute' else
time_rules.every_minute()) time_rules.every_minute())
# Check the type of the algorithm's schedule before pulling calendar # Check the type of the algorithm's schedule before pulling calendar
@@ -1819,7 +1794,7 @@ class TradingAlgorithm(object):
@data_frequency.setter @data_frequency.setter
def data_frequency(self, value): def data_frequency(self, value):
assert value in ('daily', '5-minute', 'minute') assert value in ('daily', 'minute')
self.sim_params.data_frequency = value self.sim_params.data_frequency = value
@api_method @api_method
+60 -6
View File
@@ -17,6 +17,8 @@
""" """
Cythonized Asset object. Cythonized Asset object.
""" """
import hashlib
cimport cython cimport cython
from cpython.number cimport PyNumber_Index from cpython.number cimport PyNumber_Index
from cpython.object cimport ( from cpython.object cimport (
@@ -36,6 +38,7 @@ from numpy cimport int64_t
import warnings import warnings
cimport numpy as np cimport numpy as np
from catalyst.exchange.exchange_utils import get_sid
from catalyst.utils.calendars import get_calendar from catalyst.utils.calendars import get_calendar
from catalyst.exchange.exchange_errors import InvalidSymbolError, SidHashError from catalyst.exchange.exchange_errors import InvalidSymbolError, SidHashError
@@ -395,6 +398,9 @@ cdef class TradingPair(Asset):
cdef readonly float leverage cdef readonly float leverage
cdef readonly object market_currency cdef readonly object market_currency
cdef readonly object base_currency cdef readonly object base_currency
cdef readonly object end_daily
cdef readonly object end_minute
cdef readonly object exchange_symbol
_kwargnames = frozenset({ _kwargnames = frozenset({
'sid', 'sid',
@@ -408,7 +414,11 @@ cdef class TradingPair(Asset):
'exchange_full', 'exchange_full',
'leverage', 'leverage',
'market_currency', 'market_currency',
'base_currency' 'base_currency',
'end_daily',
'end_minute',
'exchange_symbol',
'min_trade_size'
}) })
def __init__(self, def __init__(self,
object symbol, object symbol,
@@ -417,10 +427,14 @@ cdef class TradingPair(Asset):
object asset_name=None, object asset_name=None,
int sid=0, int sid=0,
float leverage=1.0, float leverage=1.0,
object end_daily=None,
object end_minute=None,
object end_date=None, object end_date=None,
object exchange_symbol=None,
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):
""" """
Replicates the Asset constructor with some built-in conventions Replicates the Asset constructor with some built-in conventions
and a new 'leverage' attribute. and a new 'leverage' attribute.
@@ -472,10 +486,14 @@ cdef class TradingPair(Asset):
:param asset_name: :param asset_name:
:param sid: :param sid:
:param leverage: :param leverage:
:param end_daily
:param end_minute
:param end_date: :param end_date:
:param exchange_symbol:
:param first_traded: :param first_traded:
:param auto_close_date: :param auto_close_date:
:param exchange_full: :param exchange_full:
:param min_trade_size:
""" """
symbol = symbol.lower() symbol = symbol.lower()
@@ -486,7 +504,7 @@ cdef class TradingPair(Asset):
if sid == 0 or sid is None: if sid == 0 or sid is None:
try: try:
sid = abs(hash(symbol)) % (10 ** 4) sid = get_sid(symbol)
except Exception as e: except Exception as e:
raise SidHashError(symbol=symbol) raise SidHashError(symbol=symbol)
@@ -509,25 +527,60 @@ 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
) )
self.leverage = leverage self.leverage = leverage
self.end_daily = end_daily
self.end_minute = end_minute
self.exchange_symbol = exchange_symbol
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}, ' \ 'Market Currency: {market_currency}, ' \
'Base Currency: {base_currency}, ' \ 'Base Currency: {base_currency}, ' \
'Exchange Leverage: {leverage}'.format( 'Exchange Leverage: {leverage}, ' \
'Minimum Trade Size: {min_trade_size} ' \
'Last daily ingestion: {end_daily} ' \
'Last minutely ingestion: {end_minute}'.format(
symbol=self.symbol, symbol=self.symbol,
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, market_currency=self.market_currency,
base_currency=self.base_currency, base_currency=self.base_currency,
leverage=self.leverage leverage=self.leverage,
min_trade_size=self.min_trade_size,
end_daily=self.end_daily,
end_minute=self.end_minute
) )
cpdef to_dict(self):
"""
Convert to a python dict.
"""
super_dict = super(TradingPair, self).to_dict()
super_dict['end_daily'] = self.end_daily
super_dict['end_minute'] = self.end_minute
super_dict['leverage'] = self.leverage
super_dict['min_trade_size'] = self.min_trade_size
return super_dict
def is_exchange_open(self, dt_minute):
"""
Parameters
----------
dt_minute: pd.Timestamp (UTC, tz-aware)
The minute to check.
Returns
-------
boolean: whether the asset's exchange is open at the given minute.
"""
#TODO: consider implementing to spot holds
return True
cpdef __reduce__(self): cpdef __reduce__(self):
""" """
Function used by pickle to determine how to serialize/deserialize this Function used by pickle to determine how to serialize/deserialize this
@@ -544,7 +597,8 @@ cdef class TradingPair(Asset):
self.end_date, self.end_date,
self.first_traded, self.first_traded,
self.auto_close_date, self.auto_close_date,
self.exchange_full)) self.exchange_full,
self.min_trade_size))
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)
+3 -1
View File
@@ -76,7 +76,9 @@ from catalyst.utils.numpy_utils import as_column
from catalyst.utils.preprocess import preprocess from catalyst.utils.preprocess import preprocess
from catalyst.utils.sqlite_utils import group_into_chunks, coerce_string_to_eng from catalyst.utils.sqlite_utils import group_into_chunks, coerce_string_to_eng
log = Logger('assets.py') from catalyst.constants import LOG_LEVEL
log = Logger('assets.py', level=LOG_LEVEL)
# A set of fields that need to be converted to strings before building an # A set of fields that need to be converted to strings before building an
# Asset to avoid unicode fields # Asset to avoid unicode fields
+18
View File
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
import os
import logbook
''' 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
+182 -62
View File
@@ -2,10 +2,12 @@ import json, time, csv
from datetime import datetime from datetime import datetime
import pandas as pd import pandas as pd
import os, time, shutil, requests, logbook import os, time, shutil, requests, logbook
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename
DT_START = int(time.mktime(datetime(2010, 1, 1, 0, 0).timetuple())) DT_START = int(time.mktime(datetime(2010, 1, 1, 0, 0).timetuple()))
DT_END = int(time.time()) DT_END = pd.to_datetime('today').value // 10 ** 9
CSV_OUT_FOLDER = '/var/tmp/catalyst/data/poloniex/' CSV_OUT_FOLDER = os.environ.get('CSV_OUT_FOLDER', '/efs/exchanges/poloniex/')
CONN_RETRIES = 2 CONN_RETRIES = 2
logbook.StderrHandler().push_application() logbook.StderrHandler().push_application()
@@ -24,13 +26,15 @@ class PoloniexCurator(object):
try: try:
os.makedirs(CSV_OUT_FOLDER) os.makedirs(CSV_OUT_FOLDER)
except Exception as e: except Exception as e:
log.error('Failed to create data folder: %s' % CSV_OUT_FOLDER) log.error('Failed to create data folder: {}'.format(
CSV_OUT_FOLDER))
log.exception(e) log.exception(e)
def get_currency_pairs(self):
''' '''
Retrieves and returns all currency pairs from the exchange Retrieves and returns all currency pairs from the exchange
''' '''
def get_currency_pairs(self):
url = self._api_path + 'command=returnTicker' url = self._api_path + 'command=returnTicker'
try: try:
@@ -46,92 +50,140 @@ class PoloniexCurator(object):
self.currency_pairs.append(ticker) self.currency_pairs.append(ticker)
self.currency_pairs.sort() self.currency_pairs.sort()
log.debug('Currency pairs retrieved successfully: %d' % (len(self.currency_pairs))) log.debug('Currency pairs retrieved successfully: {}'.format(
len(self.currency_pairs)
))
def _retrieve_tradeID_date(self, row):
''' '''
Helper function that reads tradeID and date fields from CSV readline Helper function that reads tradeID and date fields from CSV readline
''' '''
def _retrieve_tradeID_date(self, row):
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,
end=DT_END, temp=None):
''' '''
Retrieves TradeHistory from exchange for a given currencyPair between start and end dates. Retrieves TradeHistory from exchange for a given currencyPair
If no start date is provided, uses a system-wide one (beginning of time for cryptotrading) between start and end dates. If no start date is provided, uses
If no end date is provided, 'now' is used 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
limitations imposed by the provider API.
''' '''
def retrieve_trade_history(self, currencyPair, start=DT_START, end=DT_END, temp=None):
csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv' csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
''' '''
Check what data we already have on disk, reading first and last lines from file. Check what data we already have on disk, reading first and last
Data is stored on file from NEWEST to OLDEST. lines from file. Data is stored on file from NEWEST to OLDEST.
''' '''
try: try:
with open(csv_fn, 'ab+') as f: with open(csv_fn, 'ab+') as f:
f.seek(0, os.SEEK_END) f.seek(0, os.SEEK_END)
if(f.tell() > 2): # First check file is not zero size if(f.tell() > 2): # Check file size is not 0
f.seek(0) # Go to the beginning to read first line f.seek(0) # Go to start to read
last_tradeID, end_file = self._retrieve_tradeID_date(f.readline()) last_tradeID, end_file = self._retrieve_tradeID_date(f.readline())
f.seek(-2, os.SEEK_END) # Jump to the second last byte. f.seek(-2, os.SEEK_END) # Jump to the 2nd last byte
while f.read(1) != b"\n": # Until EOL is found... while f.read(1) != b"\n": # Until EOL is found...
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more. f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
first_tradeID, start_file = self._retrieve_tradeID_date(f.readline()) first_tradeID, start_file = self._retrieve_tradeID_date(f.readline())
if( first_tradeID == 1 and end_file + 3600 > DT_END ): if( end_file + 3600 * 6 > DT_END and ( first_tradeID == 1
or (currencyPair == 'BTC_HUC' and first_tradeID == 2)
or (currencyPair == 'BTC_RIC' and first_tradeID == 2)
or (currencyPair == 'BTC_XCP' and first_tradeID == 2)
or (currencyPair == 'BTC_NAV' and first_tradeID == 4569)
or (currencyPair == 'BTC_POT' and first_tradeID == 23511) ) ):
return return
except Exception as e: except Exception as e:
log.error('Error opening file: %s' % csv_fn) log.error('Error opening file: {}'.format(csv_fn))
log.exception(e) log.exception(e)
''' '''
Poloniex API limits querying TradeHistory to intervals smaller than 1 month, Poloniex API limits querying TradeHistory to intervals smaller
so we make sure that start date is never more than 1 month apart from end date than 1 month, so we make sure that start date is never more than
1 month apart from end date
''' '''
if( end - start > 2419200 ): # 60s/min * 60min/hr * 24hr/day * 28days if( end - start > 2419200 ): # 60s/min * 60min/hr * 24hr/day * 28days
newstart = end - 2419200 newstart = end - 2419200
else: else:
newstart = start newstart = start
log.debug(currencyPair+': Retrieving from '+str(newstart)+' to '+str(end) +'\t ' log.debug('{}: Retrieving from {} to {}\t {} - {}'.format(
+ time.ctime(newstart) + ' - '+ time.ctime(end)) currencyPair, str(newstart), str(end),
time.ctime(newstart), time.ctime(end)))
url = self._api_path + 'command=returnTradeHistory&currencyPair=' + currencyPair + '&start=' + str(newstart) + '&end=' + str(end) url = '{path}command=returnTradeHistory&currencyPair={pair}' \
'&start={start}&end={end}'.format(
path = self._api_path,
pair = currencyPair,
start = str(newstart),
end = str(end)
)
print url
attempts = 0
success = 0
while attempts < CONN_RETRIES:
try: try:
response = requests.get(url) response = requests.get(url)
except Exception as e: except Exception as e:
log.error('Failed to retrieve trade history data for %s' % currencyPair) log.error('Failed to retrieve trade history data for {}'.format(
currencyPair
))
log.exception(e) log.exception(e)
return None attempts += 1
else: else:
try:
if isinstance(response.json(), dict) and response.json()['error']: 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'])) log.error('Failed to to retrieve trade history data '
exit(1) '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
''' '''
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)
@@ -148,8 +200,10 @@ class PoloniexCurator(object):
item['globalTradeID'] item['globalTradeID']
]) ])
if( response.json()[-1]['tradeID'] > last_tradeID ): if( response.json()[-1]['tradeID'] > last_tradeID ):
end = pd.to_datetime( response.json()[-1]['date'], infer_datetime_format=True).value // 10 ** 9 end = pd.to_datetime( response.json()[-1]['date'],
self.retrieve_trade_history(currencyPair, start, end, temp=temp) infer_datetime_format=True).value // 10 ** 9
self.retrieve_trade_history(currencyPair, start,
end, temp=temp)
else: else:
with open(csv_fn,'rb+') as f: with open(csv_fn,'rb+') as f:
shutil.copyfileobj(f,temp) shutil.copyfileobj(f,temp)
@@ -162,7 +216,8 @@ class PoloniexCurator(object):
with open(csv_fn, 'ab') as csvfile: with open(csv_fn, 'ab') as csvfile:
csvwriter = csv.writer(csvfile) csvwriter = csv.writer(csvfile)
for item in response.json(): for item in response.json():
if( 'first_tradeID' in locals() and item['tradeID'] >= first_tradeID ): if( 'first_tradeID' in locals()
and item['tradeID'] >= first_tradeID ):
continue continue
csvwriter.writerow([ csvwriter.writerow([
item['tradeID'], item['tradeID'],
@@ -173,52 +228,71 @@ 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') # Will deal with vol separately, as ohlc() messes it up vol = df['total'].to_frame('volume') # set Vol aside
df.drop('total', axis=1, inplace=True) # Drop volume data from dataframe df.drop('total', axis=1, inplace=True) # Drop volume data
ohlc = df.resample('T').ohlc() # Resample OHLC in 1min bins ohlc = df.resample('T').ohlc() # Resample OHLC 1min
ohlc.columns = ohlc.columns.map(lambda t: t[1]) # Raname columns by dropping 'rate' ohlc.columns = ohlc.columns.map(lambda t: t[1]) # Raname columns by dropping 'rate'
closes = ohlc['close'].fillna(method='pad') # Pad forward missing 'close' closes = ohlc['close'].fillna(method='pad') # Pad fwd missing 'close'
ohlc = ohlc.apply(lambda x: x.fillna(closes)) # Fill N/A with last close ohlc = ohlc.apply(lambda x: x.fillna(closes)) # Fill N/A with last close
vol = vol.resample('T').sum().fillna(0) # Add volumes by bin vol = vol.resample('T').sum().fillna(0) # Add volumes by bin
ohlcv = pd.concat([ohlc,vol], axis=1) # Concatenate OHLC + Volume ohlcv = pd.concat([ohlc,vol], axis=1) # Concatenate OHLC + Vol
return ohlcv return ohlcv
def write_ohlcv_file(self, currencyPair):
''' '''
Generates OHLCV data file with 1minute bars from TradeHistory on disk Generates OHLCV data file with 1minute bars from TradeHistory on disk
''' '''
def write_ohlcv_file(self, currencyPair):
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. '
'Delete the file if you want to rebuild it.')
else: else:
df = pd.read_csv(csv_trades, names=['tradeID','date','type','rate','amount','total','globalTradeID'], df = pd.read_csv(csv_trades,
dtype = {'tradeID': int, 'date': str, 'type': str, 'rate': float, 'amount': float, 'total': float, 'globalTradeID': int } ) names=['tradeID',
df.drop(['tradeID','type','amount','globalTradeID'], axis=1, inplace=True) 'date',
'type',
'rate',
'amount',
'total',
'globalTradeID'],
dtype = {'tradeID': int,
'date': str,
'type': str,
'rate': float,
'amount': float,
'total': float,
'globalTradeID': int }
)
df.drop(['tradeID','type','amount','globalTradeID'],
axis=1, inplace=True)
df['date'] = pd.to_datetime(df['date'], infer_datetime_format=True) df['date'] = pd.to_datetime(df['date'], infer_datetime_format=True)
ohlcv = self.generate_ohlcv(df) ohlcv = self.generate_ohlcv(df)
try: try:
with open(csv_1min, 'ab') as csvfile: with open(csv_1min, 'w') as csvfile:
csvwriter = csv.writer(csvfile) csvwriter = csv.writer(csvfile)
for item in ohlcv.itertuples(): for item in ohlcv.itertuples():
if item.Index == 0: if item.Index == 0:
@@ -232,26 +306,72 @@ class PoloniexCurator(object):
item.volume, item.volume,
]) ])
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)
log.debug(currencyPair+': Generated 1min OHLCV data.') log.debug('{}: Generated 1min OHLCV data.'.format(currencyPair))
def onemin_to_dataframe(self, currencyPair, start, end):
''' '''
Returns a data frame for a given currencyPair from data on disk Returns a data frame for a given currencyPair from data on disk
''' '''
def onemin_to_dataframe(self, currencyPair, start, end):
csv_fn = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv' csv_fn = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
df = pd.read_csv(csv_fn, names=['date', 'open', 'high', 'low', 'close', 'volume']) df = pd.read_csv(csv_fn, names=['date',
'open',
'high',
'low',
'close',
'volume']
)
df['date'] = pd.to_datetime(df['date'],unit='s') df['date'] = pd.to_datetime(df['date'],unit='s')
df.set_index('date', inplace=True) df.set_index('date', inplace=True)
return df[start : end] return df[start : end]
def generate_symbols_json(self, filename=None):
'''
Generates a symbols.json file with corresponding start_date
for each currencyPair
'''
symbol_map = {}
if(filename is None):
filename = get_exchange_symbols_filename('poloniex')
with open(filename, 'w') as symbols:
for currencyPair in self.currency_pairs:
start = None
csv_fn = '{}crypto_trades-{}.csv'.format(
CSV_OUT_FOLDER, currencyPair)
with open(csv_fn, 'r') as f:
f.seek(0, os.SEEK_END)
if(f.tell() > 2): # Check file size is not 0
f.seek(-2, os.SEEK_END) # Jump to 2nd last byte
while f.read(1) != b"\n": # Until EOL is found...
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
start = pd.to_datetime( f.readline().split(',')[1],
infer_datetime_format=True)
if(start is None):
start = time.gmtime()
base, market = currencyPair.lower().split('_')
symbol = '{market}_{base}'.format( market=market, base=base )
symbol_map[currencyPair] = dict(
symbol = symbol,
start_date = start.strftime("%Y-%m-%d")
)
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()
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)
+2
View File
@@ -220,6 +220,8 @@ cpdef _read_bcolz_data(ctable_t table,
outbuf_as_float = outbuf.astype(float64) * .000000001 outbuf_as_float = outbuf.astype(float64) * .000000001
outbuf_as_float[where_nan] = NAN outbuf_as_float[where_nan] = NAN
results.append(outbuf_as_float) results.append(outbuf_as_float)
elif column_name in ['volume']:
results.append(outbuf.astype(float64) * .000000001)
else: else:
results.append(outbuf) results.append(outbuf)
return results return results
-78
View File
@@ -35,17 +35,6 @@ def minute_value(ndarray[long_t, ndim=1] market_opens,
return market_opens[q] + r return market_opens[q] + r
@cython.cdivision(True)
def five_minute_value(ndarray[long_t, ndim=1] market_opens,
Py_ssize_t pos,
short five_minutes_per_day):
cdef short q, r
q = cython.cdiv(pos, five_minutes_per_day)
r = cython.cmod(pos, five_minutes_per_day)
return market_opens[q] + r
def find_position_of_minute(ndarray[long_t, ndim=1] market_opens, def find_position_of_minute(ndarray[long_t, ndim=1] market_opens,
ndarray[long_t, ndim=1] market_closes, ndarray[long_t, ndim=1] market_closes,
long_t minute_val, long_t minute_val,
@@ -99,26 +88,6 @@ def find_position_of_minute(ndarray[long_t, ndim=1] market_opens,
return (market_open_loc * minutes_per_day) + delta return (market_open_loc * minutes_per_day) + delta
def find_position_of_five_minute(ndarray[long_t, ndim=1] market_opens,
ndarray[long_t, ndim=1] market_closes,
long_t five_minute_val,
short five_minutes_per_day,
bool forward_fill):
cdef Py_ssize_t market_open_loc, market_open, delta
market_open_loc = \
searchsorted(market_opens, five_minute_val, side='right') - 1
market_open = market_opens[market_open_loc]
market_close = market_closes[market_open_loc]
if not forward_fill and ((five_minute_val - market_open) >= five_minutes_per_day):
raise ValueError("Given five minutes is not between an open and a close")
delta = int_min(five_minute_val - market_open, market_close - market_open)
return (market_open_loc * five_minutes_per_day) + delta
def find_last_traded_position_internal( def find_last_traded_position_internal(
ndarray[long_t, ndim=1] market_opens, ndarray[long_t, ndim=1] market_opens,
ndarray[long_t, ndim=1] market_closes, ndarray[long_t, ndim=1] market_closes,
@@ -189,50 +158,3 @@ def find_last_traded_position_internal(
# found a trade event # found a trade event
return -1 return -1
def find_last_traded_five_minute_position_internal(
ndarray[long_t, ndim=1] market_opens,
ndarray[long_t, ndim=1] market_closes,
long_t end_five_minute,
long_t start_five_minute,
volumes,
short five_minutes_per_day):
cdef Py_ssize_t minute_pos, current_minute, q
five_minute_pos = int_min(
find_position_of_five_minute(
market_opens,
market_closes,
end_five_minute,
five_minutes_per_day,
True,
),
len(volumes) - 1,
)
while five_minute_pos >= 0:
current_five_minute = five_minute_value(
market_opens, five_minute_pos, five_minutes_per_day
)
q = cython.cdiv(five_minute_pos, five_minutes_per_day)
if current_five_minute > market_closes[q]:
five_minute_pos = find_position_of_five_minute(
market_opens,
market_closes,
market_closes[q],
five_minutes_per_day,
False,
)
continue
if current_five_minute < start_five_minute:
return -1
if volumes[five_minute_pos] != 0:
return five_minute_pos
five_minute_pos -= 1
# we've gone to the beginning of this asset's range, and still haven't
# found a trade event
return -1
+4 -27
View File
@@ -30,8 +30,10 @@ from catalyst.utils.cli import (
) )
from catalyst.utils.memoize import lazyval from catalyst.utils.memoize import lazyval
from catalyst.constants import LOG_LEVEL
logbook.StderrHandler().push_application() logbook.StderrHandler().push_application()
log = logbook.Logger(__name__) log = logbook.Logger(__name__, level=LOG_LEVEL)
DEFAULT_RETRIES = 5 DEFAULT_RETRIES = 5
@@ -60,10 +62,6 @@ class BaseBundle(object):
def minutes_per_day(self): def minutes_per_day(self):
raise NotImplementedError() raise NotImplementedError()
@lazyval
def five_minutes_per_day(self):
raise NotImplementedError()
@lazyval @lazyval
def frequencies(self): def frequencies(self):
raise NotImplementedError() raise NotImplementedError()
@@ -115,7 +113,6 @@ class BaseBundle(object):
environ, environ,
asset_db_writer, asset_db_writer,
minute_bar_writer, minute_bar_writer,
five_minute_bar_writer,
daily_bar_writer, daily_bar_writer,
adjustment_writer, adjustment_writer,
calendar, calendar,
@@ -162,7 +159,7 @@ class BaseBundle(object):
# Post-process metadata using cached symbol frames, and write to # Post-process metadata using cached symbol frames, and write to
# disk. This metadata must be written before any attempt to write # disk. This metadata must be written before any attempt to write
# either minute or 5-minute data. # minute data.
metadata = self._post_process_metadata( metadata = self._post_process_metadata(
raw_metadata, raw_metadata,
cache, cache,
@@ -170,26 +167,6 @@ class BaseBundle(object):
) )
asset_db_writer.write(metadata) asset_db_writer.write(metadata)
# Compile 5-minute symbol data if bundle supports 5-minute mode and
# persist the dataset to disk.
'''
if '5-minute' in self.frequencies:
five_minute_bar_writer.write(
self._fetch_symbol_iter(
api_key,
cache,
symbol_map,
calendar,
start_session,
end_session,
'5-minute',
retries,
),
length=len(symbol_map),
show_progress=show_progress,
)
'''
# Compile minute symbol data if bundle supports minute mode and # Compile minute symbol data if bundle supports minute mode and
# persist the dataset to disk. # persist the dataset to disk.
if 'minute' in self.frequencies: if 'minute' in self.frequencies:
-8
View File
@@ -47,10 +47,6 @@ class BaseCryptoPricingBundle(BasePricingBundle):
def minutes_per_day(self): def minutes_per_day(self):
return 1440 return 1440
@lazyval
def five_minutes_per_day(self):
return 288
@property @property
def splits(self): def splits(self):
return [] return []
@@ -68,10 +64,6 @@ class BaseEquityPricingBundle(BasePricingBundle):
def minutes_per_day(self): def minutes_per_day(self):
return 390 return 390
@lazyval
def five_minutes_per_day(self):
return 78
@property @property
def splits(self): def splits(self):
return self._splits return self._splits
+1 -31
View File
@@ -17,10 +17,6 @@ from ..us_equity_pricing import (
SQLiteAdjustmentReader, SQLiteAdjustmentReader,
SQLiteAdjustmentWriter, SQLiteAdjustmentWriter,
) )
from ..five_minute_bars import (
BcolzFiveMinuteBarReader,
BcolzFiveMinuteBarWriter,
)
from ..minute_bars import ( from ..minute_bars import (
BcolzMinuteBarReader, BcolzMinuteBarReader,
BcolzMinuteBarWriter, BcolzMinuteBarWriter,
@@ -54,11 +50,6 @@ def minute_path(bundle_name, timestr, environ=None):
environ=environ, environ=environ,
) )
def five_minute_path(bundle_name, timestr, environ=None):
return pth.data_path(
five_minute_relative(bundle_name, timestr, environ),
environ=environ,
)
def daily_path(bundle_name, timestr, environ=None): def daily_path(bundle_name, timestr, environ=None):
return pth.data_path( return pth.data_path(
@@ -92,8 +83,6 @@ def cache_relative(bundle_name, timestr, environ=None):
def daily_relative(bundle_name, timestr, environ=None): def daily_relative(bundle_name, timestr, environ=None):
return bundle_name, timestr, 'daily_equities.bcolz' return bundle_name, timestr, 'daily_equities.bcolz'
def five_minute_relative(bundle_name, timestr, environ=None):
return bundle_name, timestr, 'five_minute.bcolz'
def minute_relative(bundle_name, timestr, environ=None): def minute_relative(bundle_name, timestr, environ=None):
return bundle_name, timestr, 'minute_equities.bcolz' return bundle_name, timestr, 'minute_equities.bcolz'
@@ -206,14 +195,13 @@ RegisteredBundle = namedtuple(
'start_session', 'start_session',
'end_session', 'end_session',
'minutes_per_day', 'minutes_per_day',
'five_minutes_per_day',
'ingest', 'ingest',
'create_writers'] 'create_writers']
) )
BundleData = namedtuple( BundleData = namedtuple(
'BundleData', 'BundleData',
'asset_finder minute_bar_reader five_minute_bar_reader daily_bar_reader ' 'asset_finder minute_bar_reader daily_bar_reader '
'adjustment_reader', 'adjustment_reader',
) )
@@ -303,7 +291,6 @@ def _make_bundle_core():
bundle.ingest, bundle.ingest,
calendar_name=bundle.calendar_name, calendar_name=bundle.calendar_name,
minutes_per_day=bundle.minutes_per_day, minutes_per_day=bundle.minutes_per_day,
five_minutes_per_day=bundle.five_minutes_per_day,
start_session=start_session, start_session=start_session,
end_session=end_session, end_session=end_session,
create_writers=create_writers, create_writers=create_writers,
@@ -316,7 +303,6 @@ def _make_bundle_core():
start_session=None, start_session=None,
end_session=None, end_session=None,
minutes_per_day=1440, minutes_per_day=1440,
five_minutes_per_day=288,
create_writers=True): create_writers=True):
"""Register a data bundle ingest function. """Register a data bundle ingest function.
@@ -397,7 +383,6 @@ def _make_bundle_core():
start_session=start_session, start_session=start_session,
end_session=end_session, end_session=end_session,
minutes_per_day=minutes_per_day, minutes_per_day=minutes_per_day,
five_minutes_per_day=five_minutes_per_day,
ingest=f, ingest=f,
create_writers=create_writers, create_writers=create_writers,
) )
@@ -496,16 +481,6 @@ def _make_bundle_core():
# that it can compute the adjustment ratios for the dividends. # that it can compute the adjustment ratios for the dividends.
daily_bar_writer.write(()) daily_bar_writer.write(())
five_minute_bar_writer = BcolzFiveMinuteBarWriter(
wd.ensure_dir(*five_minute_relative(
name, timestr, environ=environ)
),
calendar,
start_session,
end_session,
five_minutes_per_day=bundle.five_minutes_per_day,
)
minute_bar_writer = BcolzMinuteBarWriter( minute_bar_writer = BcolzMinuteBarWriter(
wd.ensure_dir(*minute_relative( wd.ensure_dir(*minute_relative(
name, timestr, environ=environ) name, timestr, environ=environ)
@@ -532,7 +507,6 @@ def _make_bundle_core():
) )
else: else:
daily_bar_writer = None daily_bar_writer = None
five_minute_bar_writer = None
minute_bar_writer = None minute_bar_writer = None
asset_db_writer = None asset_db_writer = None
adjustment_db_writer = None adjustment_db_writer = None
@@ -544,7 +518,6 @@ def _make_bundle_core():
environ, environ,
asset_db_writer, asset_db_writer,
minute_bar_writer, minute_bar_writer,
five_minute_bar_writer,
daily_bar_writer, daily_bar_writer,
adjustment_db_writer, adjustment_db_writer,
calendar, calendar,
@@ -631,9 +604,6 @@ def _make_bundle_core():
minute_bar_reader=BcolzMinuteBarReader( minute_bar_reader=BcolzMinuteBarReader(
minute_path(name, timestr, environ=environ), minute_path(name, timestr, environ=environ),
), ),
five_minute_bar_reader=BcolzFiveMinuteBarReader(
five_minute_path(name, timestr, environ=environ),
),
daily_bar_reader=BcolzDailyBarReader( daily_bar_reader=BcolzDailyBarReader(
daily_path(name, timestr, environ=environ), daily_path(name, timestr, environ=environ),
), ),
+3 -1
View File
@@ -97,6 +97,8 @@ class PoloniexBundle(BaseCryptoPricingBundle):
end_date, end_date,
frequency): frequency):
# TODO: replace this with direct exchange call
# 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)
@@ -146,7 +148,6 @@ class PoloniexBundle(BaseCryptoPricingBundle):
data_frequency): data_frequency):
period_map = { period_map = {
'daily': 86400, 'daily': 86400,
# '5-minute': 300,
} }
try: try:
@@ -165,6 +166,7 @@ class PoloniexBundle(BaseCryptoPricingBundle):
return self._format_polo_query(query_params) return self._format_polo_query(query_params)
def _format_polo_query(self, query_params): def _format_polo_query(self, query_params):
# TODO: got against the exchange object
return 'https://poloniex.com/public?{query}'.format( return 'https://poloniex.com/public?{query}'.format(
query=urlencode(query_params), query=urlencode(query_params),
) )
+3 -1
View File
@@ -40,7 +40,9 @@ from catalyst.utils.cli import maybe_show_progress
from . import core as bundles from . import core as bundles
log = Logger(__name__) from catalyst.constants import 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):
+3 -32
View File
@@ -42,7 +42,6 @@ from catalyst.assets.roll_finder import (
) )
from catalyst.data.dispatch_bar_reader import ( from catalyst.data.dispatch_bar_reader import (
AssetDispatchMinuteBarReader, AssetDispatchMinuteBarReader,
AssetDispatchFiveMinuteBarReader,
AssetDispatchSessionBarReader AssetDispatchSessionBarReader
) )
from catalyst.data.resample import ( from catalyst.data.resample import (
@@ -69,7 +68,9 @@ from catalyst.errors import (
HistoryWindowStartsBeforeData, HistoryWindowStartsBeforeData,
) )
log = Logger('DataPortal') from catalyst.constants import LOG_LEVEL
log = Logger('DataPortal', level=LOG_LEVEL)
BASE_FIELDS = frozenset([ BASE_FIELDS = frozenset([
"open", "open",
@@ -120,10 +121,6 @@ class DataPortal(object):
daily data backtests or daily history calls in a minute backetest. daily data backtests or daily history calls in a minute backetest.
If a daily bar reader is not provided but a minute bar reader is, If a daily bar reader is not provided but a minute bar reader is,
the minutes will be rolled up to serve the daily requests. the minutes will be rolled up to serve the daily requests.
five_minute_reader : BcolzFiveMinuteBarReader, optional
The five minute bar reader for equities. This will be used to service
5-minute data backtests or five-minute history calls. This can be used
to serve daily calls if no daily bar reader is provided.
minute_reader : BcolzMinuteBarReader, optional minute_reader : BcolzMinuteBarReader, optional
The minute bar reader for equities. This will be used to service The minute bar reader for equities. This will be used to service
minute data backtests or minute history calls. This can be used minute data backtests or minute history calls. This can be used
@@ -150,7 +147,6 @@ class DataPortal(object):
trading_calendar, trading_calendar,
first_trading_day, first_trading_day,
daily_reader=None, daily_reader=None,
five_minute_reader=None,
minute_reader=None, minute_reader=None,
future_daily_reader=None, future_daily_reader=None,
future_minute_reader=None, future_minute_reader=None,
@@ -202,7 +198,6 @@ class DataPortal(object):
reader.last_available_dt reader.last_available_dt
for reader in [ for reader in [
minute_reader, minute_reader,
five_minute_reader,
future_minute_reader, future_minute_reader,
] ]
if reader is not None if reader is not None
@@ -214,8 +209,6 @@ class DataPortal(object):
aligned_minute_reader = self._ensure_reader_aligned( aligned_minute_reader = self._ensure_reader_aligned(
minute_reader) minute_reader)
aligned_five_minute_reader = self._ensure_reader_aligned(
five_minute_reader)
aligned_session_reader = self._ensure_reader_aligned( aligned_session_reader = self._ensure_reader_aligned(
daily_reader) daily_reader)
aligned_future_minute_reader = self._ensure_reader_aligned( aligned_future_minute_reader = self._ensure_reader_aligned(
@@ -229,13 +222,10 @@ class DataPortal(object):
} }
aligned_minute_readers = {} aligned_minute_readers = {}
aligned_five_minute_readers = {}
aligned_session_readers = {} aligned_session_readers = {}
if aligned_minute_reader is not None: if aligned_minute_reader is not None:
aligned_minute_readers[Equity] = aligned_minute_reader aligned_minute_readers[Equity] = aligned_minute_reader
if aligned_five_minute_reader is not None:
aligned_five_minute_readers[Equity] = aligned_five_minute_reader
if aligned_session_reader is not None: if aligned_session_reader is not None:
aligned_session_readers[Equity] = aligned_session_reader aligned_session_readers[Equity] = aligned_session_reader
@@ -267,13 +257,6 @@ class DataPortal(object):
self._last_available_minute, self._last_available_minute,
) )
_dispatch_five_minute_reader = AssetDispatchFiveMinuteBarReader(
self.trading_calendar,
self.asset_finder,
aligned_five_minute_readers,
self._last_available_minute,
)
_dispatch_session_reader = AssetDispatchSessionBarReader( _dispatch_session_reader = AssetDispatchSessionBarReader(
self.trading_calendar, self.trading_calendar,
self.asset_finder, self.asset_finder,
@@ -283,7 +266,6 @@ class DataPortal(object):
self._pricing_readers = { self._pricing_readers = {
'minute': _dispatch_minute_reader, 'minute': _dispatch_minute_reader,
'5-minute': _dispatch_five_minute_reader,
'daily': _dispatch_session_reader, 'daily': _dispatch_session_reader,
} }
@@ -719,17 +701,6 @@ class DataPortal(object):
spot_value=result spot_value=result
) )
def _get_five_minute_spot_value(self, asset, column, dt, ffill=False):
return self._get_minutely_spot_value(
asset,
column,
dt,
ffill,
'5-minute',
)
def _get_minute_spot_value(self, asset, column, dt, ffill=False): def _get_minute_spot_value(self, asset, column, dt, ffill=False):
return self._get_minutely_spot_value( return self._get_minutely_spot_value(
asset, asset,
-6
View File
@@ -138,12 +138,6 @@ 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 AssetDispatchFiveMinuteBarReader(AssetDispatchBarReader):
def _dt_window_size(self, start_dt, end_dt):
return len(self.trading_calendar.five_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):
File diff suppressed because it is too large Load Diff
+81 -52
View File
@@ -12,32 +12,29 @@
# 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
import logbook import logbook
import pandas as pd import pandas as pd
import numpy as np
from pandas_datareader.data import DataReader
import datetime
import time
import pytz import pytz
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 .benchmarks import get_benchmark_returns from catalyst.utils.calendars import get_calendar
from . import treasuries, treasuries_can from . import treasuries, treasuries_can
from .benchmarks import get_benchmark_returns
from ..utils.deprecate import deprecated
from ..utils.paths import ( from ..utils.paths import (
cache_root, cache_root,
data_root, data_root,
) )
from ..utils.deprecate import deprecated
from catalyst.data.bundles.poloniex import PoloniexBundle from catalyst.constants import LOG_LEVEL
from catalyst.utils.calendars import get_calendar
logger = logbook.Logger('Loader', level=LOG_LEVEL)
logger = logbook.Logger('Loader')
# Mapping from index symbol to appropriate bond data # Mapping from index symbol to appropriate bond data
INDEX_MAPPING = { INDEX_MAPPING = {
@@ -94,18 +91,27 @@ def has_data_for_dates(series_or_df, first_date, last_date):
if not isinstance(dts, pd.DatetimeIndex): if not isinstance(dts, pd.DatetimeIndex):
raise TypeError("Expected a DatetimeIndex, but got %s." % type(dts)) raise TypeError("Expected a DatetimeIndex, but got %s." % type(dts))
first, last = dts[[0, -1]].tz_localize(None) first, last = dts[[0, -1]].tz_localize(None)
return (first <= first_date.tz_localize(None)) and (last >= last_date.tz_localize(None)) return (first <= first_date.tz_localize(None)) and (
last >= last_date.tz_localize(None))
def load_crypto_market_data(trading_day=None, trading_days=None, bm_symbol='USDT_BTC',
bundle=None, bundle_data=None, environ=None):
def load_crypto_market_data(trading_day=None, trading_days=None,
bm_symbol=None, bundle=None, bundle_data=None,
environ=None, exchange=None, start_dt=None,
end_dt=None):
if trading_day is None: if trading_day is None:
trading_day = get_calendar('OPEN').trading_day trading_day = get_calendar('OPEN').trading_day
if trading_days is None:
trading_days = get_calendar('OPEN').all_sessions
first_date = trading_days[1] # TODO: consider making configurable
now = pd.Timestamp.utcnow() bm_symbol = 'btc_usdt'
# if trading_days is None:
# trading_days = get_calendar('OPEN').schedule
# if start_dt is None:
start_dt = get_calendar('OPEN').first_trading_session
if end_dt is None:
end_dt = pd.Timestamp.utcnow()
# We expect to have benchmark and treasury data that's current up until # We expect to have benchmark and treasury data that's current up until
# **two** full trading days prior to the most recently completed trading # **two** full trading days prior to the most recently completed trading
@@ -121,6 +127,7 @@ def load_crypto_market_data(trading_day=None, trading_days=None, bm_symbol='USDT
# We'll attempt to download new data if the latest entry in our cache is # We'll attempt to download new data if the latest entry in our cache is
# 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 the last
# date for which there is trading data in the bundle # date for which there is trading data in the bundle
@@ -129,19 +136,32 @@ def load_crypto_market_data(trading_day=None, trading_days=None, bm_symbol='USDT
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]
'''
last_date = trading_days[trading_days.get_loc(end_dt, method='ffill') - 1]
if exchange is None:
# This is exceptional, since placing the import at the module scope
# breaks things and it's only needed here
from catalyst.exchange.poloniex.poloniex import Poloniex
exchange = Poloniex('', '', '')
benchmark_asset = exchange.get_asset(bm_symbol)
# exchange.get_history_window() already ensures that we have the right data
# for the right dates
br = exchange.get_history_window_with_bundle(
assets=[benchmark_asset],
end_dt=last_date,
bar_count=pd.Timedelta(last_date - start_dt).days,
frequency='1d',
field='close',
data_frequency='daily',
force_auto_ingest=True)
br.columns = ['close']
br = br.pct_change(1).iloc[1:]
br.loc[start_dt] = 0
br = br.sort_index()
br = ensure_crypto_benchmark_data(
bm_symbol,
first_date,
last_date,
now,
# We need the trading_day to figure out the close prior to the first
# date so that we can compute returns for the first date.
trading_day,
bundle,
bundle_data,
environ,
)
# 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 years
# and is independent of crypto data # and is independent of crypto data
first_date_treasury = pd.Timestamp('1990-01-02', tz='UTC') first_date_treasury = pd.Timestamp('1990-01-02', tz='UTC')
@@ -149,11 +169,12 @@ def load_crypto_market_data(trading_day=None, trading_days=None, bm_symbol='USDT
bm_symbol, bm_symbol,
first_date_treasury, first_date_treasury,
last_date, last_date,
now, end_dt,
environ, environ,
) )
benchmark_returns = br[br.index.slice_indexer(first_date, last_date)] benchmark_returns = br[br.index.slice_indexer(start_dt, last_date)]
treasury_curves = tc[tc.index.slice_indexer(first_date_treasury, last_date)] treasury_curves = tc[
tc.index.slice_indexer(first_date_treasury, last_date)]
return benchmark_returns, treasury_curves return benchmark_returns, treasury_curves
@@ -251,7 +272,6 @@ def ensure_crypto_benchmark_data(symbol,
bundle, bundle,
bundle_data, bundle_data,
environ=None): environ=None):
filename = get_benchmark_filename(symbol) filename = get_benchmark_filename(symbol)
logger.info( logger.info(
@@ -285,43 +305,51 @@ def ensure_crypto_benchmark_data(symbol,
prevents users abroad from getting Catalyst to work prevents users abroad from getting Catalyst to work
''' '''
logger.info( logger.info(
('Retrieving benchmark data from bundle for {symbol!r} from {first_date} to {last_date}'), (
'Retrieving benchmark data from bundle for {symbol!r} from {first_date} to {last_date}'),
symbol=symbol, first_date=first_date, last_date=last_date) symbol=symbol, first_date=first_date, last_date=last_date)
asset = bundle_data.asset_finder.lookup_symbol(symbol=symbol,as_of_date=None) asset = bundle_data.asset_finder.lookup_symbol(symbol=symbol,
as_of_date=None)
fields = ['day', 'close'] fields = ['day', 'close']
raw = bundle_data.daily_bar_reader.load_raw_arrays( raw = bundle_data.daily_bar_reader.load_raw_arrays(
columns=fields, columns=fields,
start_date=first_date - trading_day, start_date=first_date - trading_day,
end_date=last_date, end_date=last_date,
assets=[asset, ]) assets=[asset, ])
bench_raw = pd.concat([pd.DataFrame(raw[0], columns=['date']),pd.DataFrame(raw[1], columns=['close'])], axis=1) bench_raw = pd.concat([pd.DataFrame(raw[0], columns=['date']),
pd.DataFrame(raw[1], columns=['close'])],
axis=1)
bench_raw['date'] = pd.to_datetime(bench_raw['date'], unit='s') bench_raw['date'] = pd.to_datetime(bench_raw['date'], unit='s')
bench_raw.set_index('date', inplace=True) bench_raw.set_index('date', inplace=True)
bench_raw.sort_index(inplace=True) bench_raw.sort_index(inplace=True)
bench_raw = bench_raw[pd.to_datetime(first_date - trading_day):pd.to_datetime(last_date)] bench_raw = bench_raw[
pd.to_datetime(first_date - trading_day):pd.to_datetime(
last_date)]
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} from {first_date} to {last_date}'), (
'Downloading benchmark data for {symbol!r} from {first_date} to {last_date}'),
symbol=symbol, first_date=first_date, last_date=last_date) symbol=symbol, first_date=first_date, last_date=last_date)
raise DeprecationWarning('poloniex bundle deprecated')
# Load benchmark symbol from Poloniex API # Load benchmark symbol from Poloniex API
try: # try:
bundle = PoloniexBundle() # bundle = PoloniexBundle()
bench_raw = bundle._fetch_symbol_frame( # bench_raw = bundle._fetch_symbol_frame(
None, # None,
symbol, # symbol,
get_calendar(bundle.calendar_name), # get_calendar(bundle.calendar_name),
first_date - trading_day, # first_date - trading_day,
last_date, # last_date,
'daily', # 'daily',
) # )
except (OSError, IOError, HTTPError): # except (OSError, IOError, HTTPError):
logger.exception('Failed to fetch new crypto benchmark returns') # logger.exception('Failed to fetch new crypto benchmark returns')
raise # raise
# select close column and compute percent change between days # select close column and compute percent change between days
daily_close = bench_raw[['close']] daily_close = bench_raw[['close']]
@@ -525,7 +553,8 @@ def _load_cached_data(filename, first_date, last_date, now, resource_name,
data = pd.DataFrame.from_csv(path) data = pd.DataFrame.from_csv(path)
if data.empty: if data.empty:
raise ValueError("File is empty.") raise ValueError("File is empty.")
data.index = pd.to_datetime(data.index, infer_datetime_format=True, errors='coerce' ).tz_localize('UTC') data.index = pd.to_datetime(data.index, infer_datetime_format=True,
errors='coerce').tz_localize('UTC')
if has_data_for_dates(data, first_date, last_date): if has_data_for_dates(data, first_date, last_date):
return data return data
+3 -1
View File
@@ -44,8 +44,9 @@ from catalyst.utils.calendars import get_calendar
from catalyst.utils.cli import maybe_show_progress from catalyst.utils.cli import maybe_show_progress
from catalyst.utils.memoize import lazyval from catalyst.utils.memoize import lazyval
from catalyst.constants import LOG_LEVEL
logger = logbook.Logger('MinuteBars') logger = logbook.Logger('MinuteBars', level=LOG_LEVEL)
US_EQUITIES_MINUTES_PER_DAY = 390 US_EQUITIES_MINUTES_PER_DAY = 390
FUTURES_MINUTES_PER_DAY = 1440 FUTURES_MINUTES_PER_DAY = 1440
@@ -1353,6 +1354,7 @@ class H5MinuteBarUpdateReader(MinuteBarUpdateReader):
path : str path : str
The path of the HDF5 file from which to source data. The path of the HDF5 file from which to source data.
""" """
def __init__(self, path): def __init__(self, path):
self._panel = pd.read_hdf(path) self._panel = pd.read_hdf(path)
+3 -1
View File
@@ -83,7 +83,9 @@ from catalyst.utils.cli import (
from ._equities import _compute_row_slices, _read_bcolz_data from ._equities import _compute_row_slices, _read_bcolz_data
from ._adjustments import load_adjustments_from_sqlite from ._adjustments import load_adjustments_from_sqlite
logger = logbook.Logger('UsEquityPricing') from catalyst.constants import LOG_LEVEL
logger = logbook.Logger('UsEquityPricing', level=LOG_LEVEL)
OHLC = frozenset(['open', 'high', 'low', 'close']) OHLC = frozenset(['open', 'high', 'low', 'close'])
OHLCV = frozenset(['open', 'high', 'low', 'close', 'volume']) OHLCV = frozenset(['open', 'high', 'low', 'close', 'volume'])
@@ -0,0 +1,275 @@
from logbook import Logger
from catalyst.api import (
record,
order,
symbol,
get_open_orders
)
from catalyst.exchange.stats_utils import get_pretty_stats
from catalyst.utils.run_algo import run_algorithm
algo_namespace = 'arbitrage_eth_btc'
log = Logger(algo_namespace)
def initialize(context):
log.info('initializing arbitrage algorithm')
# The context contains a new "exchanges" attribute which is a dictionary
# of exchange objects by exchange name. This allow easy access to the
# exchanges.
context.buying_exchange = context.exchanges['poloniex']
context.selling_exchange = context.exchanges['bitfinex']
context.trading_pair_symbol = 'eth_btc'
context.trading_pairs = dict()
# Note the second parameter of the symbol() method
# Passing the exchange name here returns a TradingPair object including
# the exchange information. This allow all other operations using
# the TradingPair to target the correct exchange.
context.trading_pairs[context.buying_exchange] = \
symbol('eth_btc', context.buying_exchange.name)
context.trading_pairs[context.selling_exchange] = \
symbol(context.trading_pair_symbol, context.selling_exchange.name)
context.entry_points = [
dict(gap=0.03, amount=0.05),
dict(gap=0.04, amount=0.1),
dict(gap=0.05, amount=0.5),
]
context.exit_points = [
dict(gap=-0.02, amount=0.5),
]
context.SLIPPAGE_ALLOWED = 0.02
pass
def place_orders(context, amount, buying_price, selling_price, action):
"""
This method will always place two orders of the same amount to keep
the currency position the same as it moves between the two exchanges.
:param context: TradingAlgorithm
:param amount: float
The trading pair amount to trade on both exchanges.
:param buying_price: float
The current trading pair price on the buying exchange.
:param selling_price: float
The current trading pair price on the selling exchange.
:param action: string
"enter": buys on the buying exchange and sells on the selling exchange
"exit": buys on the selling exchange and sells on the buying exchange
:return:
"""
if action == 'enter':
enter_exchange = context.buying_exchange
entry_price = buying_price
exit_exchange = context.selling_exchange
exit_price = selling_price
elif action == 'exit':
enter_exchange = context.selling_exchange
entry_price = selling_price
exit_exchange = context.buying_exchange
exit_price = buying_price
else:
raise ValueError('invalid order action')
base_currency = enter_exchange.base_currency
base_currency_amount = enter_exchange.portfolio.cash
exit_balances = exit_exchange.get_balances()
exit_currency = context.trading_pairs[
context.selling_exchange].market_currency
if exit_currency in exit_balances:
market_currency_amount = exit_balances[exit_currency]
else:
log.warn(
'the selling exchange {exchange_name} does not hold '
'currency {currency}'.format(
exchange_name=exit_exchange.name,
currency=exit_currency
)
)
return
if base_currency_amount < (amount * entry_price):
adj_amount = base_currency_amount / entry_price
log.warn(
'not enough {base_currency} ({base_currency_amount}) to buy '
'{amount}, adjusting the amount to {adj_amount}'.format(
base_currency=base_currency,
base_currency_amount=base_currency_amount,
amount=amount,
adj_amount=adj_amount
)
)
amount = adj_amount
elif market_currency_amount < amount:
log.warn(
'not enough {currency} ({currency_amount}) to sell '
'{amount}, aborting'.format(
currency=exit_currency,
currency_amount=market_currency_amount,
amount=amount
)
)
return
adj_buy_price = entry_price * (1 + context.SLIPPAGE_ALLOWED)
log.info(
'buying {amount} {trading_pair} on {exchange_name} with price '
'limit {limit_price}'.format(
amount=amount,
trading_pair=context.trading_pair_symbol,
exchange_name=enter_exchange.name,
limit_price=adj_buy_price
)
)
order(
asset=context.trading_pairs[enter_exchange],
amount=amount,
limit_price=adj_buy_price
)
adj_sell_price = exit_price * (1 - context.SLIPPAGE_ALLOWED)
log.info(
'selling {amount} {trading_pair} on {exchange_name} with price '
'limit {limit_price}'.format(
amount=-amount,
trading_pair=context.trading_pair_symbol,
exchange_name=exit_exchange.name,
limit_price=adj_sell_price
)
)
order(
asset=context.trading_pairs[exit_exchange],
amount=-amount,
limit_price=adj_sell_price
)
pass
def handle_data(context, data):
log.info('handling bar {}'.format(data.current_dt))
buying_price = data.current(
context.trading_pairs[context.buying_exchange], 'price')
log.info('price on buying exchange {exchange}: {price}'.format(
exchange=context.buying_exchange.name.upper(),
price=buying_price,
))
selling_price = data.current(
context.trading_pairs[context.selling_exchange], 'price')
log.info('price on selling exchange {exchange}: {price}'.format(
exchange=context.selling_exchange.name.upper(),
price=selling_price,
))
# If for example,
# selling price = 50
# buying price = 25
# expected gap = 1
# If follows that,
# selling price - buying price / buying price
# 50 - 25 / 25 = 1
gap = (selling_price - buying_price) / buying_price
log.info(
'the price gap: {gap} ({gap_percent}%)'.format(
gap=gap,
gap_percent=gap * 100
)
)
record(buying_price=buying_price, selling_price=selling_price, gap=gap)
# Waiting for orders to close before initiating new ones
for exchange in context.trading_pairs:
asset = context.trading_pairs[exchange]
orders = get_open_orders(asset)
if orders:
log.info(
'found {order_count} open orders on {exchange_name} '
'skipping bar until all open orders execute'.format(
order_count=len(orders),
exchange_name=exchange.name
)
)
return
# Consider the least ambitious entry point first
# Override of wider gap is found
entry_points = sorted(
context.entry_points,
key=lambda point: point['gap'],
)
buy_amount = None
for entry_point in entry_points:
if gap > entry_point['gap']:
buy_amount = entry_point['amount']
if buy_amount:
log.info('found buy trigger for amount: {}'.format(buy_amount))
place_orders(
context=context,
amount=buy_amount,
buying_price=buying_price,
selling_price=selling_price,
action='enter'
)
else:
# Consider the narrowest exit gap first
# Override of wider gap is found
exit_points = sorted(
context.exit_points,
key=lambda point: point['gap'],
reverse=True
)
sell_amount = None
for exit_point in exit_points:
if gap < exit_point['gap']:
sell_amount = exit_point['amount']
if sell_amount:
log.info('found sell trigger for amount: {}'.format(sell_amount))
place_orders(
context=context,
amount=sell_amount,
buying_price=buying_price,
selling_price=selling_price,
action='exit'
)
def analyze(context, stats):
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
pass
run_algorithm(
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='poloniex,bitfinex',
live=True,
algo_namespace=algo_namespace,
base_currency='btc',
live_graph=False
)
+32 -15
View File
@@ -14,7 +14,9 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import pandas as pd
from catalyst import run_algorithm
from catalyst.api import ( from catalyst.api import (
order_target_value, order_target_value,
symbol, symbol,
@@ -23,21 +25,18 @@ from catalyst.api import (
get_open_orders, get_open_orders,
) )
def initialize(context): def initialize(context):
context.ASSET_NAME = 'USDT_BTC' context.ASSET_NAME = 'btc_usd'
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
@@ -56,10 +55,11 @@ def handle_data(context, data):
context.is_buying = False context.is_buying = False
# Retrieve current asset price from pricing data # Retrieve current asset price from pricing data
price = data[context.asset].price price = data.current(context.asset, 'price')
# 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,
@@ -70,12 +70,13 @@ def handle_data(context, data):
record( record(
price=price, price=price,
volume=data[context.asset].volume, volume=data.current(context.asset, 'volume'),
cash=cash, cash=cash,
starting_cash=context.portfolio.starting_cash, starting_cash=context.portfolio.starting_cash,
leverage=context.account.leverage, leverage=context.account.leverage,
) )
def analyze(context=None, results=None): def analyze(context=None, results=None):
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@@ -86,18 +87,19 @@ def analyze(context=None, results=None):
ax2 = plt.subplot(612, sharex=ax1) ax2 = plt.subplot(612, sharex=ax1)
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME)) ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
(context.TICK_SIZE * results[['price']]).plot(ax=ax2) results[['price']].plot(ax=ax2)
trans = results.ix[[t != [] for t in results.transactions]] trans = results.ix[[t != [] for t in results.transactions]]
buys = trans.ix[ buys = trans.ix[
[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)
@@ -135,3 +137,18 @@ def analyze(context=None, results=None):
# Show the plot. # Show the plot.
plt.gcf().set_size_inches(18, 8) plt.gcf().set_size_inches(18, 8)
plt.show() plt.show()
if __name__ == '__main__':
run_algorithm(
capital_base=10000,
data_frequency='minute',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
algo_namespace='buy_and_hodl',
base_currency='usd',
start=pd.to_datetime('2017-11-01', utc=True),
end=pd.to_datetime('2017-11-10', utc=True),
)
+29
View File
@@ -0,0 +1,29 @@
'''
This is a very simple example referenced in the beginner's tutorial:
https://enigmampc.github.io/catalyst/beginner-tutorial.html
Run this example, by executing the following from your terminal:
catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle
If you want to run this code using another exchange, make sure that
the asset is available on that exchange. For example, if you were to run
it for exchange Poloniex, you would need to edit the following line:
context.asset = symbol('btc_usdt') # note 'usdt' instead of 'usd'
and specify exchange poloniex as follows:
catalyst run -f buy_btc_simple.py -x poloniex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle
To see which assets are available on each exchange, visit:
https://www.enigma.co/catalyst/status
'''
from catalyst.api import order, record, symbol
def initialize(context):
context.asset = symbol('btc_usd')
def handle_data(context, data):
order(context.asset, 1)
record(btc = data.current(context.asset, 'price'))
+8 -5
View File
@@ -27,7 +27,7 @@ log = Logger(algo_namespace)
def initialize(context): def initialize(context):
log.info('initializing algo') log.info('initializing algo')
context.ASSET_NAME = 'XRP_USD' context.ASSET_NAME = 'XRP_USDT'
context.asset = symbol(context.ASSET_NAME) context.asset = symbol(context.ASSET_NAME)
context.TARGET_POSITIONS = 5000 context.TARGET_POSITIONS = 5000
@@ -38,6 +38,8 @@ def initialize(context):
context.retry_update_portfolio = 10 context.retry_update_portfolio = 10
context.retry_order = 5 context.retry_order = 5
context.swallow_errors = True
context.errors = [] context.errors = []
pass pass
@@ -49,6 +51,7 @@ def _handle_data(context, data):
bar_count=20, bar_count=20,
frequency='15m' frequency='15m'
) )
rsi = talib.RSI(prices.values, timeperiod=14)[-1] rsi = talib.RSI(prices.values, timeperiod=14)[-1]
log.info('got rsi: {}'.format(rsi)) log.info('got rsi: {}'.format(rsi))
@@ -135,11 +138,11 @@ def _handle_data(context, data):
def handle_data(context, data): def handle_data(context, data):
log.info('handling bar {}'.format(data.current_dt)) log.info('handling bar {}'.format(data.current_dt))
# try: try:
_handle_data(context, data) _handle_data(context, data)
# except Exception as e: except Exception as e:
# log.warn('aborting the bar on error {}'.format(e)) log.warn('aborting the bar on error {}'.format(e))
# context.errors.append(e) context.errors.append(e)
log.info('completed bar {}, total execution errors {}'.format( log.info('completed bar {}, total execution errors {}'.format(
data.current_dt, data.current_dt,
+168
View File
@@ -0,0 +1,168 @@
import talib
from logbook import Logger
import pandas as pd
from catalyst.api import (
order,
order_target_percent,
symbol,
record,
get_open_orders,
)
from catalyst.exchange.stats_utils import get_pretty_stats
from catalyst.utils.run_algo import run_algorithm
algo_namespace = 'buy_the_dip_live'
log = Logger('buy low sell high')
def initialize(context):
log.info('initializing algo')
context.ASSET_NAME = 'btc_usdt'
context.asset = symbol(context.ASSET_NAME)
context.TARGET_POSITIONS = 30
context.PROFIT_TARGET = 0.1
context.SLIPPAGE_ALLOWED = 0.02
context.retry_check_open_orders = 10
context.retry_update_portfolio = 10
context.retry_order = 5
context.errors = []
pass
def _handle_data(context, data):
price = data.current(context.asset, 'price')
log.info('got price {price}'.format(price=price))
prices = data.history(
context.asset,
fields='price',
bar_count=20,
frequency='1d'
)
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
log.info('got rsi: {}'.format(rsi))
# Buying more when RSI is low, this should lower our cost basis
if rsi <= 30:
buy_increment = 1
elif rsi <= 40:
buy_increment = 0.5
elif rsi <= 70:
buy_increment = 0.2
else:
buy_increment = 0.1
cash = context.portfolio.cash
log.info('base currency available: {cash}'.format(cash=cash))
record(
price=price,
rsi=rsi,
)
orders = get_open_orders(context.asset)
if orders:
log.info('skipping bar until all open orders execute')
return
is_buy = False
cost_basis = None
if context.asset in context.portfolio.positions:
position = context.portfolio.positions[context.asset]
cost_basis = position.cost_basis
log.info(
'found {amount} positions with cost basis {cost_basis}'.format(
amount=position.amount,
cost_basis=cost_basis
)
)
if position.amount >= context.TARGET_POSITIONS:
log.info('reached positions target: {}'.format(position.amount))
return
if price < cost_basis:
is_buy = True
elif position.amount > 0 and \
price > cost_basis * (1 + context.PROFIT_TARGET):
profit = (price * position.amount) - (cost_basis * position.amount)
log.info('closing position, taking profit: {}'.format(profit))
order_target_percent(
asset=context.asset,
target=0,
limit_price=price * (1 - context.SLIPPAGE_ALLOWED),
)
else:
log.info('no buy or sell opportunity found')
else:
is_buy = True
if is_buy:
if buy_increment is None:
log.info('the rsi is too high to consider buying {}'.format(rsi))
return
if price * buy_increment > cash:
log.info('not enough base currency to consider buying')
return
log.info(
'buying position cheaper than cost basis {} < {}'.format(
price,
cost_basis
)
)
order(
asset=context.asset,
amount=buy_increment,
limit_price=price * (1 + context.SLIPPAGE_ALLOWED)
)
def handle_data(context, data):
log.info('handling bar {}'.format(data.current_dt))
# try:
_handle_data(context, data)
# except Exception as e:
# log.warn('aborting the bar on error {}'.format(e))
# context.errors.append(e)
log.info('completed bar {}, total execution errors {}'.format(
data.current_dt,
len(context.errors)
))
if len(context.errors) > 0:
log.info('the errors:\n{}'.format(context.errors))
def analyze(context, stats):
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
pass
run_algorithm(
capital_base=100000,
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='poloniex',
start=pd.to_datetime('2017-5-01', utc=True),
end=pd.to_datetime('2017-10-16', utc=True),
base_currency='usdt',
data_frequency='daily'
)
# run_algorithm(
# initialize=initialize,
# handle_data=handle_data,
# analyze=analyze,
# exchange_name='poloniex',
# live=True,
# algo_namespace=algo_namespace,
# base_currency='btc'
# )
+153
View File
@@ -0,0 +1,153 @@
import numpy as np
import pandas as pd
from logbook import Logger
import matplotlib.pyplot as plt
from catalyst import run_algorithm
from catalyst.api import (order, record, symbol, order_target_percent,
get_open_orders)
from catalyst.exchange.stats_utils import extract_transactions
NAMESPACE = 'dual_moving_average'
log = Logger(NAMESPACE)
def initialize(context):
context.i = 0
context.asset = symbol('ltc_usd')
context.base_price = None
def handle_data(context, data):
# define the windows for the moving averages
short_window = 50
long_window = 200
# Skip as many bars as long_window to properly compute the average
context.i += 1
if context.i < long_window:
return
# Compute moving averages calling data.history() for each
# moving average with the appropriate parameters. We choose to use
# minute bars for this simulation -> freq="1m"
# Returns a pandas dataframe.
short_mavg = data.history(context.asset, 'price',
bar_count=short_window, frequency="1m").mean()
long_mavg = data.history(context.asset, 'price',
bar_count=long_window, frequency="1m").mean()
# Let's keep the price of our asset in a more handy variable
price = data.current(context.asset, 'price')
# If base_price is not set, we use the current value. This is the
# price at the first bar which we reference to calculate price_change.
if context.base_price is None:
context.base_price = price
price_change = (price - context.base_price) / context.base_price
# Save values for later inspection
record(price=price,
cash=context.portfolio.cash,
price_change=price_change,
short_mavg=short_mavg,
long_mavg=long_mavg)
# Since we are using limit orders, some orders may not execute immediately
# we wait until all orders are executed before considering more trades.
orders = get_open_orders(context.asset)
if len(orders) > 0:
return
# Exit if we cannot trade
if not data.can_trade(context.asset):
return
# We check what's our position on our portfolio and trade accordingly
pos_amount = context.portfolio.positions[context.asset].amount
# Trading logic
if short_mavg > long_mavg and pos_amount == 0:
# we buy 100% of our portfolio for this asset
order_target_percent(context.asset, 1)
elif short_mavg < long_mavg and pos_amount > 0:
# we sell all our positions for this asset
order_target_percent(context.asset, 0)
def analyze(context, perf):
# Get the base_currency that was passed as a parameter to the simulation
base_currency = context.exchanges.values()[0].base_currency.upper()
# First 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),
)
+275
View File
@@ -0,0 +1,275 @@
# For this example, we're going to write a simple momentum script. When the
# stock goes up quickly, we're going to buy; when it goes down quickly, we're
# going to sell. Hopefully we'll ride the waves.
import os
import tempfile
import time
import pandas as pd
import talib
from logbook import Logger
from catalyst import run_algorithm
from catalyst.api import symbol, record, order_target_percent, get_open_orders
from catalyst.exchange.stats_utils import extract_transactions
# We give a name to the algorithm which Catalyst will use to persist its state.
# In this example, Catalyst will create the `.catalyst/data/live_algos`
# directory. If we stop and start the algorithm, Catalyst will resume its
# state using the files included in the folder.
from catalyst.utils.paths import ensure_directory
NAMESPACE = 'mean_reversion_simple'
log = Logger(NAMESPACE)
# To run an algorithm in Catalyst, you need two functions: initialize and
# handle_data.
def initialize(context):
# This initialize function sets any data or variables that you'll use in
# your algorithm. For instance, you'll want to define the trading pair (or
# trading pairs) you want to backtest. You'll also want to define any
# parameters or values you're going to use.
# In our example, we're looking at Ether in USD Tether.
context.neo_eth = symbol('neo_eth')
context.base_price = None
context.current_day = None
context.RSI_OVERSOLD = 50
context.RSI_OVERBOUGHT = 80
context.CANDLE_SIZE = '5T'
context.start_time = time.time()
def handle_data(context, data):
# This handle_data function is where the real work is done. Our data is
# minute-level tick data, and each minute is called a frame. This function
# runs on each frame of the data.
# We flag the first period of each day.
# Since cryptocurrencies trade 24/7 the `before_trading_starts` handle
# would only execute once. This method works with minute and daily
# frequencies.
today = data.current_dt.floor('1D')
if today != context.current_day:
context.traded_today = False
context.current_day = today
# We're computing the volume-weighted-average-price of the security
# defined above, in the context.neo_eth variable. For this example, we're
# using three bars on the 15 min bars.
# The frequency attribute determine the bar size. We use this convention
# for the frequency alias:
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
prices = data.history(
context.neo_eth,
fields='close',
bar_count=50,
frequency=context.CANDLE_SIZE
)
# Ta-lib calculates various technical indicator based on price and
# volume arrays.
# In this example, we are comp
rsi = talib.RSI(prices.values, timeperiod=14)
# We need a variable for the current price of the security to compare to
# the average. Since we are requesting two fields, data.current()
# returns a DataFrame with
current = data.current(context.neo_eth, fields=['close', 'volume'])
price = current['close']
# If base_price is not set, we use the current value. This is the
# price at the first bar which we reference to calculate price_change.
if context.base_price is None:
context.base_price = price
price_change = (price - context.base_price) / context.base_price
cash = context.portfolio.cash
# Now that we've collected all current data for this frame, we use
# the record() method to save it. This data will be available as
# a parameter of the analyze() function for further analysis.
record(
price=price,
volume=current['volume'],
price_change=price_change,
rsi=rsi[-1],
cash=cash
)
# We are trying to avoid over-trading by limiting our trades to
# one per day.
if context.traded_today:
return
# Since we are using limit orders, some orders may not execute immediately
# we wait until all orders are executed before considering more trades.
orders = get_open_orders(context.neo_eth)
if len(orders) > 0:
return
# Exit if we cannot trade
if not data.can_trade(context.neo_eth):
return
# Another powerful built-in feature of the Catalyst backtester is the
# portfolio object. The portfolio object tracks your positions, cash,
# cost basis of specific holdings, and more. In this line, we calculate
# how long or short our position is at this minute.
pos_amount = context.portfolio.positions[context.neo_eth].amount
if rsi[-1] <= context.RSI_OVERSOLD and pos_amount == 0:
log.info(
'{}: buying - price: {}, rsi: {}'.format(
data.current_dt, price, rsi[-1]
)
)
# Set a style for limit orders,
limit_price = price * 1.005
order_target_percent(
context.neo_eth, 1, limit_price=limit_price
)
context.traded_today = True
elif rsi[-1] >= context.RSI_OVERBOUGHT and pos_amount > 0:
log.info(
'{}: selling - price: {}, rsi: {}'.format(
data.current_dt, price, rsi[-1]
)
)
limit_price = price * 0.995
order_target_percent(
context.neo_eth, 0, limit_price=limit_price
)
context.traded_today = True
def analyze(context=None, perf=None):
end = time.time()
log.info('elapsed time: {}'.format(end - context.start_time))
import matplotlib.pyplot as plt
# The base currency of the algo exchange
base_currency = context.exchanges.values()[0].base_currency.upper()
# Plot the portfolio value over time.
ax1 = plt.subplot(611)
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
ax1.set_ylabel('Portfolio Value ({})'.format(base_currency))
# Plot the price increase or decrease over time.
ax2 = plt.subplot(612, sharex=ax1)
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
ax2.set_ylabel('{asset} ({base})'.format(
asset=context.neo_eth.symbol, base=base_currency
))
transaction_df = extract_transactions(perf)
if not transaction_df.empty:
buy_df = transaction_df[transaction_df['amount'] > 0]
sell_df = transaction_df[transaction_df['amount'] < 0]
ax2.scatter(
buy_df.index.to_pydatetime(),
perf.loc[buy_df.index.floor('1 min'), 'price'],
marker='^',
s=100,
c='green',
label=''
)
ax2.scatter(
sell_df.index.to_pydatetime(),
perf.loc[sell_df.index.floor('1 min'), 'price'],
marker='v',
s=100,
c='red',
label=''
)
ax4 = plt.subplot(613, sharex=ax1)
perf.loc[:, 'cash'].plot(
ax=ax4, label='Base Currency ({})'.format(base_currency)
)
ax4.set_ylabel('Cash ({})'.format(base_currency))
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
ax5 = plt.subplot(614, sharex=ax1)
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
ax5.set_ylabel('Percent Change')
ax6 = plt.subplot(615, sharex=ax1)
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
ax6.axhline(70, color='darkgoldenrod')
ax6.axhline(30, color='darkgoldenrod')
if not transaction_df.empty:
ax6.scatter(
buy_df.index.to_pydatetime(),
perf.loc[buy_df.index.floor('1 min'), 'rsi'],
marker='^',
s=100,
c='green',
label=''
)
ax6.scatter(
sell_df.index.to_pydatetime(),
perf.loc[sell_df.index.floor('1 min'), 'rsi'],
marker='v',
s=100,
c='red',
label=''
)
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
pass
if __name__ == '__main__':
# The execution mode: backtest or live
MODE = 'backtest'
if MODE == 'backtest':
folder = os.path.join(
tempfile.gettempdir(), 'catalyst', NAMESPACE
)
ensure_directory(folder)
timestr = time.strftime('%Y%m%d-%H%M%S')
out = os.path.join(folder, '{}.p'.format(timestr))
# catalyst run -f catalyst/examples/mean_reversion_simple.py -x poloniex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000
run_algorithm(
capital_base=10000,
data_frequency='minute',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
algo_namespace=NAMESPACE,
base_currency='usd',
start=pd.to_datetime('2017-10-01', utc=True),
end=pd.to_datetime('2017-11-10', utc=True),
output=out
)
log.info('saved perf stats: {}'.format(out))
elif MODE == 'live':
run_algorithm(
capital_base=0.5,
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bittrex',
live=True,
algo_namespace=NAMESPACE,
base_currency='eth',
live_graph=False
)
+276
View File
@@ -0,0 +1,276 @@
from datetime import timedelta
import pandas as pd
import numpy as np
import talib
from logbook import Logger
from catalyst.api import (
order,
symbol,
record,
get_open_orders,
)
from catalyst.exchange.stats_utils import crossover, crossunder
from catalyst.utils.run_algo import run_algorithm
algo_namespace = 'rsi'
log = Logger(algo_namespace)
def initialize(context):
log.info('initializing algo')
context.asset = symbol('eth_btc')
context.base_price = None
context.MAX_HOLDINGS = 0.2
context.RSI_OVERSOLD = 30
context.RSI_OVERSOLD_BBANDS = 45
context.RSI_OVERBOUGHT_BBANDS = 55
context.SLIPPAGE_ALLOWED = 0.03
context.TARGET = 0.15
context.STOP_LOSS = 0.1
context.STOP = 0.03
context.position = None
context.last_bar = None
context.errors = []
pass
def _handle_buy_sell_decision(context, data, signal, price):
orders = get_open_orders(context.asset)
if len(orders) > 0:
log.info('skipping bar until all open orders execute')
return
positions = context.portfolio.positions
if context.position is None and context.asset in positions:
position = positions[context.asset]
context.position = dict(
cost_basis=position['cost_basis'],
amount=position['amount'],
stop=None
)
action = None
if context.position is not None:
cost_basis = context.position['cost_basis']
amount = context.position['amount']
log.info(
'found {amount} positions with cost basis {cost_basis}'.format(
amount=amount,
cost_basis=cost_basis
)
)
stop = context.position['stop']
target = cost_basis * (1 + context.TARGET)
if price >= target:
context.position['cost_basis'] = price
context.position['stop'] = context.STOP
stop_target = context.STOP_LOSS if stop is None else context.STOP
if price < cost_basis * (1 - stop_target):
log.info('executing stop loss')
order(
asset=context.asset,
amount=-amount,
limit_price=price * (1 - context.SLIPPAGE_ALLOWED),
)
action = 0
context.position = None
else:
if signal == 'long':
log.info('opening position')
buy_amount = context.MAX_HOLDINGS / price
order(
asset=context.asset,
amount=buy_amount,
limit_price=price * (1 + context.SLIPPAGE_ALLOWED),
)
context.position = dict(
cost_basis=price,
amount=buy_amount,
stop=None
)
action = 0
def _handle_data_rsi_only(context, data):
price = data.current(context.asset, 'close')
log.info('got price {price}'.format(price=price))
if price is np.nan:
log.warn('no pricing data')
return
if context.base_price is None:
context.base_price = price
try:
prices = data.history(
context.asset,
fields='price',
bar_count=17,
frequency='30T'
)
except Exception as e:
log.warn('historical data not available: '.format(e))
return
rsi = talib.RSI(prices.values, timeperiod=16)[-1]
log.info('got rsi {}'.format(rsi))
signal = None
if rsi < context.RSI_OVERSOLD:
signal = 'long'
# Making sure that the price is still current
price = data.current(context.asset, 'close')
cash = context.portfolio.cash
log.info(
'base currency available: {cash}, cap: {cap}'.format(
cash=cash,
cap=context.MAX_HOLDINGS
)
)
volume = data.current(context.asset, 'volume')
price_change = (price - context.base_price) / context.base_price
record(
price=price,
price_change=price_change,
rsi=rsi,
volume=volume,
cash=cash,
starting_cash=context.portfolio.starting_cash,
leverage=context.account.leverage,
)
_handle_buy_sell_decision(context, data, signal, price)
def handle_data(context, data):
dt = data.current_dt
if context.last_bar is None or (
context.last_bar + timedelta(minutes=15)) <= dt:
context.last_bar = dt
else:
return
log.info('BAR {}'.format(dt))
try:
_handle_data_rsi_only(context, data)
except Exception as e:
log.warn('aborting the bar on error {}'.format(e))
context.errors.append(e)
if len(context.errors) > 0:
log.info('the errors:\n{}'.format(context.errors))
def analyze(context=None, results=None):
import matplotlib.pyplot as plt
base_currency = context.exchanges.values()[0].base_currency.upper()
# Plot the portfolio and asset data.
ax1 = plt.subplot(611)
results.loc[:, 'portfolio_value'].plot(ax=ax1)
ax1.set_ylabel('Portfolio Value ({})'.format(base_currency))
ax2 = plt.subplot(612, sharex=ax1)
results.loc[:, 'price'].plot(ax=ax2)
ax2.set_ylabel('{asset} ({base})'.format(
asset=context.asset.symbol, base=base_currency
))
trans = results.loc[[t != [] for t in results.transactions], :]
buys = trans.loc[[t[0]['amount'] > 0 for t in trans.transactions], :]
sells = trans.loc[[t[0]['amount'] < 0 for t in trans.transactions], :]
# buys = results.loc[results['action'] == 1, :]
# sells = results.loc[results['action'] == 0, :]
ax2.plot(
buys.index,
results.loc[buys.index, 'price'],
'^',
markersize=10,
color='g',
)
ax2.plot(
sells.index,
results.loc[sells.index, 'price'],
'v',
markersize=10,
color='r',
)
ax3 = plt.subplot(613, sharex=ax1)
results.loc[:, ['alpha', 'beta']].plot(ax=ax3)
ax3.set_ylabel('Alpha / Beta ')
ax4 = plt.subplot(614, sharex=ax1)
results.loc[:, ['starting_cash', 'cash']].plot(ax=ax4)
ax4.set_ylabel('Base Currency ({})'.format(base_currency))
results['algorithm'] = results.loc[:, 'algorithm_period_return']
ax5 = plt.subplot(615, sharex=ax1)
results.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
ax5.set_ylabel('Percent Change')
ax6 = plt.subplot(616, sharex=ax1)
results.loc[:, 'rsi'].plot(ax=ax6)
ax6.set_ylabel('RSI')
ax6.plot(
buys.index,
results.loc[buys.index, 'rsi'],
'^',
markersize=10,
color='g',
)
ax6.plot(
sells.index,
results.loc[sells.index, 'rsi'],
'v',
markersize=10,
color='r',
)
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
pass
# run_algorithm(
# initialize=initialize,
# handle_data=handle_data,
# analyze=analyze,
# exchange_name='bittrex',
# live=True,
# algo_namespace=algo_namespace,
# base_currency='btc',
# live_graph=False
# )
# Backtest
run_algorithm(
capital_base=0.5,
data_frequency='minute',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='poloniex',
algo_namespace=algo_namespace,
base_currency='btc',
start=pd.to_datetime('2017-9-1', utc=True),
end=pd.to_datetime('2017-10-1', utc=True),
)
+133
View File
@@ -0,0 +1,133 @@
import talib
import pandas as pd
from catalyst import run_algorithm
from catalyst.api import symbol, record
from catalyst.exchange.stats_utils import get_pretty_stats, \
extract_transactions
def initialize(context):
print('initializing')
context.asset = symbol('neo_usd')
context.base_price = None
def handle_data(context, data):
print('handling bar: {}'.format(data.current_dt))
price = data.current(context.asset, 'close')
print('got price {price}'.format(price=price))
try:
prices = data.history(
context.asset,
fields='price',
bar_count=14,
frequency='15T'
)
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
print('got rsi: {}'.format(rsi))
except Exception as e:
print(e)
# If base_price is not set, we use the current value. This is the
# price at the first bar which we reference to calculate price_change.
if context.base_price is None:
context.base_price = price
price_change = (price - context.base_price) / context.base_price
cash = context.portfolio.cash
# Now that we've collected all current data for this frame, we use
# the record() method to save it. This data will be available as
# a parameter of the analyze() function for further analysis.
record(
price=price,
price_change=price_change,
cash=cash
)
def analyze(context, perf):
import matplotlib.pyplot as plt
print('the stats: {}'.format(get_pretty_stats(perf)))
# The base currency of the algo exchange
base_currency = context.exchanges.values()[0].base_currency.upper()
# Plot the portfolio value over time.
ax1 = plt.subplot(611)
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
ax1.set_ylabel('Portfolio Value ({})'.format(base_currency))
# Plot the price increase or decrease over time.
ax2 = plt.subplot(612, sharex=ax1)
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
ax2.set_ylabel('{asset} ({base})'.format(
asset=context.asset.symbol, base=base_currency
))
transaction_df = extract_transactions(perf)
if not transaction_df.empty:
buy_df = transaction_df[transaction_df['amount'] > 0]
sell_df = transaction_df[transaction_df['amount'] < 0]
ax2.scatter(
buy_df.index.to_pydatetime(),
perf.loc[buy_df.index, 'price'],
marker='^',
s=100,
c='green',
label=''
)
ax2.scatter(
sell_df.index.to_pydatetime(),
perf.loc[sell_df.index, 'price'],
marker='v',
s=100,
c='red',
label=''
)
ax4 = plt.subplot(613, sharex=ax1)
perf.loc[:, 'cash'].plot(
ax=ax4, label='Base Currency ({})'.format(base_currency)
)
ax4.set_ylabel('Cash ({})'.format(base_currency))
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
ax5 = plt.subplot(614, sharex=ax1)
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
ax5.set_ylabel('Percent Change')
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
pass
run_algorithm(
capital_base=250,
start=pd.to_datetime('2017-11-1 0:00', utc=True),
end=pd.to_datetime('2017-11-10 23:59', utc=True),
data_frequency='daily',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
algo_namespace='simple_loop',
base_currency='usd'
)
# run_algorithm(
# initialize=initialize,
# handle_data=handle_data,
# analyze=None,
# exchange_name='poloniex',
# live=True,
# algo_namespace='simple_loop',
# base_currency='eth',
# live_graph=False
+129
View File
@@ -0,0 +1,129 @@
"""
Requires Catalyst version 0.3.0 or above
Tested on Catalyst version 0.3.3
These example aims to provide and easy way for users to learn how to collect data from the different exchanges.
You simply need to specify the exchange and the market that you want to focus on.
You will all see how to create a universe and filter it base on the exchange and the market you desire.
The example prints out the closing price of all the pairs for a given market-exchange every 30 minutes.
The example also contains the ohlcv minute data for the past seven days which could be used to create indicators
Use this as the backbone to create your own trading strategies.
Variables lookback date and date are used to ensure data for a coin existed on the lookback period specified.
"""
import numpy as np
import pandas as pd
from datetime import timedelta
from catalyst import run_algorithm
from catalyst.exchange.exchange_utils import get_exchange_symbols
from catalyst.api import (
symbols,
)
def initialize(context):
context.i = -1 # counts the minutes
context.exchange = context.exchanges.values()[0].name.lower() # exchange name
context.base_currency = context.exchanges.values()[0].base_currency.lower() # market base currency
def handle_data(context, data):
context.i += 1
lookback_days = 7 # 7 days
# current date formatted into a string
today = data.current_dt
date, time = today.strftime('%Y-%m-%d %H:%M:%S').split(' ')
lookback_date = today - timedelta(days=lookback_days) # subtract the amount of days specified in lookback
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[0] # get only the date as a string
# update universe everyday
new_day = 60 * 24 # assuming data_frequency='minute'
if not context.i % new_day:
context.universe = universe(context, lookback_date, date)
# get data every 30 minutes
minutes = 30
one_day_in_minutes = 1440 # 1440 assumes data_frequency='minute'
lookback = one_day_in_minutes / minutes * lookback_days # get N lookback_days of history data
if not context.i % minutes and context.universe:
# we iterate for every pair in the current universe
for coin in context.coins:
pair = str(coin.symbol)
# 30 minute interval ohlcv data (the standard data required for candlestick or indicators/signals)
# 30T means 30 minutes re-sampling of one minute data. change to your desire time interval.
opened = fill(data.history(coin, 'open', bar_count=lookback, frequency='30T')).values
high = fill(data.history(coin, 'high', bar_count=lookback, frequency='30T')).values
low = fill(data.history(coin, 'low', bar_count=lookback, frequency='30T')).values
close = fill(data.history(coin, 'price', bar_count=lookback, frequency='30T')).values
volume = fill(data.history(coin, 'volume', bar_count=lookback, frequency='30T')).values
# close[-1] is the equivalent to current price
# displays the minute price for each pair every 30 minutes
print(today, pair, opened[-1], high[-1], low[-1], close[-1], volume[-1])
# ----------------------------------------------------------------------------------------------------------
# -------------------------------------- Insert Your Strategy Here -----------------------------------------
# ----------------------------------------------------------------------------------------------------------
def analyze(context=None, results=None):
pass
# Get the universe for a given exchange and a given base_currency market
# Example: Poloniex BTC Market
def universe(context, lookback_date, current_date):
json_symbols = get_exchange_symbols(context.exchange) # get all the pairs for the exchange
universe_df = pd.DataFrame.from_dict(json_symbols).transpose().astype(str) # convert into a dataframe
universe_df['base_currency'] = universe_df.apply(lambda row: row.symbol.split('_')[1],
axis=1)
universe_df['market_currency'] = universe_df.apply(lambda row: row.symbol.split('_')[0],
axis=1)
# Filter all the exchange pairs to only the ones for a give base currency
universe_df = universe_df[universe_df['base_currency'] == context.base_currency]
# Filter all the pairs to ensure that pair existed in the current date range
universe_df = universe_df[universe_df.start_date < lookback_date]
universe_df = universe_df[universe_df.end_daily >= current_date]
context.coins = symbols(*universe_df.symbol) # convert all the pairs to symbols
# print(universe_df.symbol.tolist())
return universe_df.symbol.tolist()
# Replace all NA, NAN or infinite values with its nearest value
def fill(series):
if isinstance(series, pd.Series):
return series.replace([np.inf, -np.inf], np.nan).ffill().bfill()
elif isinstance(series, np.ndarray):
return pd.Series(series).replace([np.inf, -np.inf], np.nan).ffill().bfill().values
else:
return series
if __name__ == '__main__':
start_date = pd.to_datetime('2017-11-10', utc=True)
end_date = pd.to_datetime('2017-11-13', utc=True)
performance = run_algorithm(start=start_date, end=end_date,
capital_base=100.0, # amount of base_currency, not always in dollars unless usd
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
data_frequency='minute',
base_currency='btc',
live=False,
live_graph=False,
algo_namespace='simple_universe')
"""
Run in Terminal (inside catalyst environment):
python simple_universe.py
"""
+364
View File
@@ -0,0 +1,364 @@
# Run Command
# catalyst run --start 2017-1-1 --end 2017-11-1 -o talib_simple.pickle -f talib_simple.py -x poloniex
#
# Description
# Simple TALib Example showing how to use various indicators in you strategy
# Based loosly on https://github.com/mellertson/talib-macd-example/blob/master/talib-macd-matplotlib-example.py
import os
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import talib as ta
from logbook import Logger
from matplotlib.dates import date2num
from matplotlib.finance import candlestick_ohlc
from catalyst import run_algorithm
from catalyst.api import (
order,
order_target_percent,
symbol,
)
from catalyst.exchange.stats_utils import get_pretty_stats
algo_namespace = 'talib_sample'
log = Logger(algo_namespace)
def initialize(context):
log.info('Starting TALib Simple Example')
context.ASSET_NAME = 'BTC_USDT'
context.asset = symbol(context.ASSET_NAME)
context.ORDER_SIZE = 10
context.SLIPPAGE_ALLOWED = 0.05
context.swallow_errors = True
context.errors = []
# Bars to look at per iteration should be bigger than SMA_SLOW
context.BARS = 365
context.COUNT = 0
# Technical Analysis Settings
context.SMA_FAST = 50
context.SMA_SLOW = 100
context.RSI_PERIOD = 14
context.RSI_OVER_BOUGHT = 80
context.RSI_OVER_SOLD = 20
context.RSI_AVG_PERIOD = 15
context.MACD_FAST = 12
context.MACD_SLOW = 26
context.MACD_SIGNAL = 9
context.STOCH_K = 14
context.STOCH_D = 3
context.STOCH_OVER_BOUGHT = 80
context.STOCH_OVER_SOLD = 20
pass
def _handle_data(context, data):
# Get price, open, high, low, close
prices = data.history(
context.asset,
bar_count=context.BARS,
fields=['price', 'open', 'high', 'low', 'close'],
frequency='1d')
# Create a analysis data frame
analysis = pd.DataFrame(index=prices.index)
# SMA FAST
analysis['sma_f'] = ta.SMA(prices.close.as_matrix(), context.SMA_FAST)
# SMA SLOW
analysis['sma_s'] = ta.SMA(prices.close.as_matrix(), context.SMA_SLOW)
# Relative Strength Index
analysis['rsi'] = ta.RSI(prices.close.as_matrix(), context.RSI_PERIOD)
# RSI SMA
analysis['sma_r'] = ta.SMA(analysis.rsi.as_matrix(),
context.RSI_AVG_PERIOD)
# MACD, MACD Signal, MACD Histogram
analysis['macd'], analysis['macdSignal'], analysis['macdHist'] = ta.MACD(
prices.close.as_matrix(), fastperiod=context.MACD_FAST,
slowperiod=context.MACD_SLOW, signalperiod=context.MACD_SIGNAL)
# Stochastics %K %D
# %K = (Current Close - Lowest Low)/(Highest High - Lowest Low) * 100
# %D = 3-day SMA of %K
analysis['stoch_k'], analysis['stoch_d'] = ta.STOCH(
prices.high.as_matrix(), prices.low.as_matrix(),
prices.close.as_matrix(), slowk_period=context.STOCH_K,
slowd_period=context.STOCH_D)
# SMA FAST over SLOW Crossover
analysis['sma_test'] = np.where(analysis.sma_f > analysis.sma_s, 1, 0)
# MACD over Signal Crossover
analysis['macd_test'] = np.where((analysis.macd > analysis.macdSignal), 1,
0)
# Stochastics OVER BOUGHT & Decreasing
analysis['stoch_over_bought'] = np.where(
(analysis.stoch_k > context.STOCH_OVER_BOUGHT) & (
analysis.stoch_k > analysis.stoch_k.shift(1)), 1, 0)
# Stochastics OVER SOLD & Increasing
analysis['stoch_over_sold'] = np.where(
(analysis.stoch_k < context.STOCH_OVER_SOLD) & (
analysis.stoch_k > analysis.stoch_k.shift(1)), 1, 0)
# RSI OVER BOUGHT & Decreasing
analysis['rsi_over_bought'] = np.where(
(analysis.rsi > context.RSI_OVER_BOUGHT) & (
analysis.rsi < analysis.rsi.shift(1)), 1, 0)
# RSI OVER SOLD & Increasing
analysis['rsi_over_sold'] = np.where(
(analysis.rsi < context.RSI_OVER_SOLD) & (
analysis.rsi > analysis.rsi.shift(1)), 1, 0)
# Save the prices and analysis to send to analyze
context.prices = prices
context.analysis = analysis
context.price = data.current(context.asset, 'price')
makeOrders(context, analysis)
# Log the values of this bar
logAnalysis(analysis)
def handle_data(context, data):
log.info('handling bar {}'.format(data.current_dt))
try:
_handle_data(context, data)
except Exception as e:
log.warn('aborting the bar on error {}'.format(e))
context.errors.append(e)
log.info('completed bar {}, total execution errors {}'.format(
data.current_dt,
len(context.errors)
))
if len(context.errors) > 0:
log.info('the errors:\n{}'.format(context.errors))
def analyze(context, results):
# Save results in CSV file
filename = os.path.splitext(os.path.basename('talib_simple'))[0]
results.to_csv(filename + '.csv')
log.info('the daily stats:\n{}'.format(get_pretty_stats(results)))
chart(context, context.prices, context.analysis, results)
pass
def makeOrders(context, analysis):
if context.asset in context.portfolio.positions:
# Current position
position = context.portfolio.positions[context.asset]
if (position == 0):
log.info('Position Zero')
return
# Cost Basis
cost_basis = position.cost_basis
log.info(
'Holdings: {amount} @ {cost_basis}'.format(
amount=position.amount,
cost_basis=cost_basis
)
)
# Sell when holding and got sell singnal
if isSell(context, analysis):
profit = (context.price * position.amount) - (
cost_basis * position.amount)
order_target_percent(
asset=context.asset,
target=0,
limit_price=context.price * (1 - context.SLIPPAGE_ALLOWED),
)
log.info(
'Sold {amount} @ {price} Profit: {profit}'.format(
amount=position.amount,
price=context.price,
profit=profit
)
)
else:
log.info('no buy or sell opportunity found')
else:
# Buy when not holding and got buy signal
if isBuy(context, analysis):
order(
asset=context.asset,
amount=context.ORDER_SIZE,
limit_price=context.price * (1 + context.SLIPPAGE_ALLOWED)
)
log.info(
'Bought {amount} @ {price}'.format(
amount=context.ORDER_SIZE,
price=context.price
)
)
def isBuy(context, analysis):
# Bullish SMA Crossover
if (getLast(analysis, 'sma_test') == 1):
# Bullish MACD
if (getLast(analysis, 'macd_test') == 1):
return True
# # Bullish Stochastics
# if(getLast(analysis, 'stoch_over_sold') == 1):
# return True
# # Bullish RSI
# if(getLast(analysis, 'rsi_over_sold') == 1):
# return True
return False
def isSell(context, analysis):
# Bearish SMA Crossover
if (getLast(analysis, 'sma_test') == 0):
# Bearish MACD
if (getLast(analysis, 'macd_test') == 0):
return True
# # Bearish Stochastics
# if(getLast(analysis, 'stoch_over_bought') == 0):
# return True
# # Bearish RSI
# if(getLast(analysis, 'rsi_over_bought') == 0):
# return True
return False
def chart(context, prices, analysis, results):
results.portfolio_value.plot()
# Data for matplotlib finance plot
dates = date2num(prices.index.to_pydatetime())
# Create the Open High Low Close Tuple
prices_ohlc = [tuple([dates[i],
prices.open[i],
prices.high[i],
prices.low[i],
prices.close[i]]) for i in range(len(dates))]
fig = plt.figure(figsize=(14, 18))
# Draw the candle sticks
ax1 = fig.add_subplot(411)
ax1.set_ylabel(context.ASSET_NAME, size=20)
candlestick_ohlc(ax1, prices_ohlc, width=0.4, colorup='g', colordown='r')
# Draw Moving Averages
analysis.sma_f.plot(ax=ax1, c='r')
analysis.sma_s.plot(ax=ax1, c='g')
# RSI
ax2 = fig.add_subplot(412)
ax2.set_ylabel('RSI', size=12)
analysis.rsi.plot(ax=ax2, c='g',
label='Period: ' + str(context.RSI_PERIOD))
analysis.sma_r.plot(ax=ax2, c='r',
label='MA: ' + str(context.RSI_AVG_PERIOD))
ax2.axhline(y=30, c='b')
ax2.axhline(y=50, c='black')
ax2.axhline(y=70, c='b')
ax2.set_ylim([0, 100])
handles, labels = ax2.get_legend_handles_labels()
ax2.legend(handles, labels)
# Draw MACD computed with Talib
ax3 = fig.add_subplot(413)
ax3.set_ylabel('MACD: ' + str(context.MACD_FAST) + ', ' + str(
context.MACD_SLOW) + ', ' + str(context.MACD_SIGNAL), size=12)
analysis.macd.plot(ax=ax3, color='b', label='Macd')
analysis.macdSignal.plot(ax=ax3, color='g', label='Signal')
analysis.macdHist.plot(ax=ax3, color='r', label='Hist')
ax3.axhline(0, lw=2, color='0')
handles, labels = ax3.get_legend_handles_labels()
ax3.legend(handles, labels)
# Stochastic plot
ax4 = fig.add_subplot(414)
ax4.set_ylabel('Stoch (k,d)', size=12)
analysis.stoch_k.plot(ax=ax4, label='stoch_k:' + str(context.STOCH_K),
color='r')
analysis.stoch_d.plot(ax=ax4, label='stoch_d:' + str(context.STOCH_D),
color='g')
handles, labels = ax4.get_legend_handles_labels()
ax4.legend(handles, labels)
ax4.axhline(y=20, c='b')
ax4.axhline(y=50, c='black')
ax4.axhline(y=80, c='b')
plt.show()
def logAnalysis(analysis):
# Log only the last value in the array
log.info('- sma_f: {:.2f}'.format(getLast(analysis, 'sma_f')))
log.info('- sma_s: {:.2f}'.format(getLast(analysis, 'sma_s')))
log.info('- rsi: {:.2f}'.format(getLast(analysis, 'rsi')))
log.info('- sma_r: {:.2f}'.format(getLast(analysis, 'sma_r')))
log.info('- macd: {:.2f}'.format(getLast(analysis, 'macd')))
log.info(
'- macdSignal: {:.2f}'.format(getLast(analysis, 'macdSignal')))
log.info('- macdHist: {:.2f}'.format(getLast(analysis, 'macdHist')))
log.info('- stoch_k: {:.2f}'.format(getLast(analysis, 'stoch_k')))
log.info('- stoch_d: {:.2f}'.format(getLast(analysis, 'stoch_d')))
log.info('- sma_test: {}'.format(getLast(analysis, 'sma_test')))
log.info('- macd_test: {}'.format(getLast(analysis, 'macd_test')))
log.info('- stoch_over_bought: {}'.format(
getLast(analysis, 'stoch_over_bought')))
log.info(
'- stoch_over_sold: {}'.format(getLast(analysis, 'stoch_over_sold')))
log.info('- rsi_over_bought: {}'.format(
getLast(analysis, 'rsi_over_bought')))
log.info(
'- rsi_over_sold: {}'.format(getLast(analysis, 'rsi_over_sold')))
def getLast(arr, name):
return arr[name][arr[name].index[-1]]
if __name__ == '__main__':
run_algorithm(
capital_base=10000,
data_frequency='daily',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='poloniex',
base_currency='usdt',
start=pd.to_datetime('2016-11-1', utc=True),
end=pd.to_datetime('2017-11-10', utc=True),
)
+22 -14
View File
@@ -1,11 +1,12 @@
from logbook import Logger from logbook import Logger
log = Logger('AssetFinderExchange') from catalyst.constants import LOG_LEVEL
log = Logger('AssetFinderExchange', level=LOG_LEVEL)
class AssetFinderExchange(object): class AssetFinderExchange(object):
def __init__(self, exchange): def __init__(self):
self.exchange = exchange
self._asset_cache = {} self._asset_cache = {}
@property @property
@@ -40,14 +41,15 @@ class AssetFinderExchange(object):
SidsNotFound SidsNotFound
When a requested sid is not found and default_none=False. When a requested sid is not found and default_none=False.
""" """
for sid in sids: # for sid in sids:
if sid in self._asset_cache: # if sid in self._asset_cache:
log.info('got asset from cache: {}'.format(sid)) # log.debug('got asset from cache: {}'.format(sid))
else: # else:
log.info('fetching asset: {}'.format(sid)) # log.debug('fetching asset: {}'.format(sid))
return list() return list()
def lookup_symbol(self, symbol, as_of_date, fuzzy=False): def lookup_symbol(self, symbol, exchange, data_frequency=None,
as_of_date=None, fuzzy=False):
"""Lookup an asset by symbol. """Lookup an asset by symbol.
Parameters Parameters
@@ -81,11 +83,17 @@ class AssetFinderExchange(object):
there are multiple candidates for the given ``symbol`` on the there are multiple candidates for the given ``symbol`` on the
``as_of_date``. ``as_of_date``.
""" """
log.debug('looking up symbol: {}'.format(symbol)) log.debug('looking up symbol: {} {}'.format(symbol, exchange.name))
if data_frequency is not None:
key = ','.join([exchange.name, symbol, data_frequency])
if symbol in self._asset_cache:
return self._asset_cache[symbol]
else: else:
asset = self.exchange.get_asset(symbol) key = ','.join([exchange.name, symbol])
self._asset_cache[symbol] = asset
if key in self._asset_cache:
return self._asset_cache[key]
else:
asset = exchange.get_asset(symbol, data_frequency)
self._asset_cache[key] = asset
return asset return asset
+200 -24
View File
@@ -1,4 +1,5 @@
import base64 import base64
import datetime
import hashlib import hashlib
import hmac import hmac
import json import json
@@ -13,14 +14,16 @@ import six
from catalyst.assets._assets import TradingPair from catalyst.assets._assets import TradingPair
from logbook import Logger from logbook import Logger
# from websocket import create_connection
from catalyst.exchange.exchange import Exchange from catalyst.exchange.exchange import Exchange
from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.exchange_errors import ( from catalyst.exchange.exchange_errors import (
ExchangeRequestError, ExchangeRequestError,
InvalidHistoryFrequencyError, InvalidHistoryFrequencyError,
InvalidOrderStyle, OrderCancelError) InvalidOrderStyle, OrderCancelError)
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \ from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
ExchangeStopLimitOrder, ExchangeStopOrder 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.finance.order import Order, ORDER_STATUS
from catalyst.protocol import Account from catalyst.protocol import Account
@@ -30,7 +33,9 @@ requests.adapters.DEFAULT_RETRIES = 20
BITFINEX_URL = 'https://api.bitfinex.com' BITFINEX_URL = 'https://api.bitfinex.com'
log = Logger('Bitfinex') from catalyst.constants import LOG_LEVEL
log = Logger('Bitfinex', level=LOG_LEVEL)
warning_logger = Logger('AlgoWarning') warning_logger = Logger('AlgoWarning')
@@ -40,13 +45,29 @@ class Bitfinex(Exchange):
self.key = key self.key = key
self.secret = secret.encode('UTF-8') self.secret = secret.encode('UTF-8')
self.name = 'bitfinex' self.name = 'bitfinex'
self.assets = {} self.color = 'green'
self.assets = dict()
self.load_assets() self.load_assets()
self.local_assets = dict()
self.load_assets(is_local=True)
self.base_currency = base_currency self.base_currency = base_currency
self._portfolio = portfolio self._portfolio = portfolio
self.minute_writer = None self.minute_writer = None
self.minute_reader = 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 = 9
self.request_cpt = dict()
self.bundle = ExchangeBundle(self.name)
def _request(self, operation, data, version='v1'): def _request(self, operation, data, version='v1'):
payload_object = { payload_object = {
'request': '/{}/{}'.format(version, operation), 'request': '/{}/{}'.format(version, operation),
@@ -173,6 +194,7 @@ class Bitfinex(Exchange):
def get_balances(self): def get_balances(self):
log.debug('retrieving wallets balances') log.debug('retrieving wallets balances')
try: try:
self.ask_request()
response = self._request('balances', None) response = self._request('balances', None)
balances = response.json() balances = response.json()
except Exception as e: except Exception as e:
@@ -223,7 +245,8 @@ class Bitfinex(Exchange):
# TODO: fetch account data and keep in cache # TODO: fetch account data and keep in cache
return None return None
def get_candles(self, data_frequency, assets, bar_count=None): def get_candles(self, freq, assets, bar_count=None,
start_dt=None, end_dt=None):
""" """
Retrieve OHLVC candles from Bitfinex Retrieve OHLVC candles from Bitfinex
@@ -237,34 +260,40 @@ class Bitfinex(Exchange):
'1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D', '14D', '1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D', '14D',
'1M' '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)
)
)
# TODO: use BcolzMinuteBarReader to read from cache allowed_frequencies = ['1T', '5T', '15T', '30T', '60T', '180T',
freq_match = re.match(r'([0-9].*)(m|h|d)', data_frequency, re.M | re.I) '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: if freq_match:
number = int(freq_match.group(1)) number = int(freq_match.group(1))
unit = freq_match.group(2) unit = freq_match.group(2)
if unit == 'd': if unit == 'T':
converted_unit = 'D' if number in [60, 180, 360, 720]:
number = number / 60
converted_unit = 'h'
else:
converted_unit = 'm'
else: else:
converted_unit = unit converted_unit = unit
frequency = '{}{}'.format(number, converted_unit) frequency = '{}{}'.format(number, converted_unit)
allowed_frequencies = ['1m', '5m', '15m', '30m', '1h', '3h', '6h',
'12h', '1D', '7D', '14D', '1M']
if frequency not in allowed_frequencies:
raise InvalidHistoryFrequencyError(
frequency=data_frequency
)
elif data_frequency == 'minute':
frequency = '1m'
elif data_frequency == 'daily':
frequency = '1D'
else: else:
raise InvalidHistoryFrequencyError( raise InvalidHistoryFrequencyError(frequency=freq)
frequency=data_frequency
)
# Making sure that assets are iterable # Making sure that assets are iterable
asset_list = [assets] if isinstance(assets, TradingPair) else assets asset_list = [assets] if isinstance(assets, TradingPair) else assets
@@ -280,11 +309,27 @@ class Bitfinex(Exchange):
if bar_count: if bar_count:
is_list = True is_list = True
url += '/hist?limit={}'.format(int(bar_count)) 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: else:
is_list = False is_list = False
url += '/last' url += '/last'
try: try:
self.ask_request()
response = requests.get(url) response = requests.get(url)
except Exception as e: except Exception as e:
raise ExchangeRequestError(error=e) raise ExchangeRequestError(error=e)
@@ -298,6 +343,9 @@ class Bitfinex(Exchange):
candles = response.json() candles = response.json()
def ohlc_from_candle(candle): def ohlc_from_candle(candle):
last_traded = pd.Timestamp.utcfromtimestamp(
candle[0] / 1000.0)
last_traded = last_traded.replace(tzinfo=pytz.UTC)
ohlc = dict( ohlc = dict(
open=np.float64(candle[1]), open=np.float64(candle[1]),
high=np.float64(candle[3]), high=np.float64(candle[3]),
@@ -305,8 +353,7 @@ class Bitfinex(Exchange):
close=np.float64(candle[2]), close=np.float64(candle[2]),
volume=np.float64(candle[5]), volume=np.float64(candle[5]),
price=np.float64(candle[2]), price=np.float64(candle[2]),
last_traded=pd.Timestamp.utcfromtimestamp( last_traded=last_traded
candle[0] / 1000.0)
) )
return ohlc return ohlc
@@ -367,6 +414,7 @@ class Bitfinex(Exchange):
date = pd.Timestamp.utcnow() date = pd.Timestamp.utcnow()
try: try:
self.ask_request()
response = self._request('order/new', req) response = self._request('order/new', req)
order_status = response.json() order_status = response.json()
except Exception as e: except Exception as e:
@@ -408,6 +456,7 @@ class Bitfinex(Exchange):
orders for this asset. orders for this asset.
""" """
try: try:
self.ask_request()
response = self._request('orders', None) response = self._request('orders', None)
order_statuses = response.json() order_statuses = response.json()
except Exception as e: except Exception as e:
@@ -419,7 +468,7 @@ class Bitfinex(Exchange):
order_statuses['message']) order_statuses['message'])
) )
orders = list() orders = []
for order_status in order_statuses: for order_status in order_statuses:
order, executed_price = self._create_order(order_status) order, executed_price = self._create_order(order_status)
if asset is None or asset == order.sid: if asset is None or asset == order.sid:
@@ -442,6 +491,7 @@ class Bitfinex(Exchange):
The order object. The order object.
""" """
try: try:
self.ask_request()
response = self._request( response = self._request(
'order/status', {'order_id': int(order_id)}) 'order/status', {'order_id': int(order_id)})
order_status = response.json() order_status = response.json()
@@ -467,6 +517,7 @@ class Bitfinex(Exchange):
if isinstance(order_param, Order) else order_param if isinstance(order_param, Order) else order_param
try: try:
self.ask_request()
response = self._request('order/cancel', {'order_id': order_id}) response = self._request('order/cancel', {'order_id': order_id})
status = response.json() status = response.json()
except Exception as e: except Exception as e:
@@ -491,6 +542,7 @@ class Bitfinex(Exchange):
log.debug('fetching tickers {}'.format(symbols)) log.debug('fetching tickers {}'.format(symbols))
try: try:
self.ask_request()
response = requests.get( response = requests.get(
'{url}/v2/tickers?symbols={symbols}'.format( '{url}/v2/tickers?symbols={symbols}'.format(
url=self.url, url=self.url,
@@ -506,7 +558,10 @@ class Bitfinex(Exchange):
response.content) response.content)
) )
try:
tickers = response.json() tickers = response.json()
except Exception as e:
raise ExchangeRequestError(error=e)
ticks = dict() ticks = dict()
for index, ticker in enumerate(tickers): for index, ticker in enumerate(tickers):
@@ -527,3 +582,124 @@ class Bitfinex(Exchange):
log.debug('got tickers {}'.format(ticks)) log.debug('got tickers {}'.format(ticks))
return ticks return ticks
def generate_symbols_json(self, filename=None, source_dates=False):
symbol_map = {}
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()
time.sleep(60 / self.max_requests_per_minute)
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 = int(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
+17 -4
View File
@@ -1,4 +1,17 @@
{ {
"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": { "btcusd": {
"symbol": "btc_usd", "symbol": "btc_usd",
"start_date": "2010-01-01" "start_date": "2010-01-01"
@@ -17,19 +30,19 @@
}, },
"ethusd": { "ethusd": {
"symbol": "eth_usd", "symbol": "eth_usd",
"start_date": "2010-01-01" "start_date": "2017-01-01"
}, },
"ethbtc": { "ethbtc": {
"symbol": "eth_btc", "symbol": "eth_btc",
"start_date": "2010-01-01" "start_date": "2017-01-01"
}, },
"etcbtc": { "etcbtc": {
"symbol": "etc_btc", "symbol": "etc_btc",
"start_date": "2010-01-01" "start_date": "2017-01-01"
}, },
"etcusd": { "etcusd": {
"symbol": "etc_usd", "symbol": "etc_usd",
"start_date": "2010-01-01" "start_date": "2017-01-01"
}, },
"rrtusd": { "rrtusd": {
"symbol": "rrt_usd", "symbol": "rrt_usd",
+140 -42
View File
@@ -1,36 +1,56 @@
import json import json
import time
import pandas as pd import pandas as pd
from catalyst.assets._assets import TradingPair from catalyst.assets._assets import TradingPair
from logbook import Logger from logbook import Logger
from six.moves import urllib from six.moves import urllib
from catalyst.constants import LOG_LEVEL
from catalyst.exchange.bittrex.bittrex_api import Bittrex_api from catalyst.exchange.bittrex.bittrex_api import Bittrex_api
from catalyst.exchange.exchange import Exchange from catalyst.exchange.exchange import Exchange
from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \ from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
ExchangeRequestError, InvalidOrderStyle, OrderNotFound, OrderCancelError, \ ExchangeRequestError, InvalidOrderStyle, OrderNotFound, OrderCancelError, \
CreateOrderError 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.execution import LimitOrder, StopLimitOrder
from catalyst.finance.order import Order, ORDER_STATUS from catalyst.finance.order import Order, ORDER_STATUS
log = Logger('Bittrex') # TODO: consider using this: https://github.com/mondeja/bittrex_v2
log = Logger('Bittrex', level=LOG_LEVEL)
URL2 = 'https://bittrex.com/Api/v2.0' URL2 = 'https://bittrex.com/Api/v2.0'
class Bittrex(Exchange): class Bittrex(Exchange):
def __init__(self, key, secret, base_currency, portfolio=None): def __init__(self, key, secret, base_currency, portfolio=None):
self.api = Bittrex_api(key=key, secret=secret.encode('UTF-8')) self.api = Bittrex_api(key=key, secret=secret)
self.name = 'bittrex' self.name = 'bittrex'
self.color = 'blue'
self.base_currency = base_currency self.base_currency = base_currency
self._portfolio = portfolio 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_writer = None
self.minute_reader = None self.minute_reader = None
self.assets = dict() self.assets = dict()
self.load_assets() self.load_assets()
self.local_assets = dict()
self.load_assets(is_local=True)
self.bundle = ExchangeBundle(self.name)
@property @property
def account(self): def account(self):
pass pass
@@ -50,42 +70,24 @@ class Bittrex(Exchange):
""" """
return exchange_symbol.lower() return exchange_symbol.lower()
def fetch_symbol_map(self):
"""
Since Bittrex gives us a complete dictionary of symbols,
we can build the symbol map ad-hoc as opposed to maintaining
a static file. We must be careful with mapping any unconventional
symbol name as appropriate.
:return symbol_map:
"""
symbol_map = dict()
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'])
)
symbol_map[exchange_symbol] = dict(
symbol=symbol,
start_date=pd.to_datetime(market['Created'], utc=True)
)
return symbol_map
def get_balances(self): def get_balances(self):
balances = self.api.getbalances()
try: try:
log.debug('retrieving wallet balances') log.debug('retrieving wallet balances')
balances = self.api.getbalances() self.ask_request()
except Exception as e: except Exception as e:
raise ExchangeRequestError(error=e) raise ExchangeRequestError(error=e)
std_balances = dict() std_balances = dict()
try:
for balance in balances: for balance in balances:
currency = balance['Currency'].lower() currency = balance['Currency'].lower()
std_balances[currency] = balance['Available'] std_balances[currency] = balance['Available']
except TypeError:
raise ExchangeRequestError(error=balances)
return std_balances return std_balances
def create_order(self, asset, amount, is_buy, style): def create_order(self, asset, amount, is_buy, style):
@@ -98,6 +100,7 @@ class Bittrex(Exchange):
price = style.get_limit_price(is_buy) price = style.get_limit_price(is_buy)
try: try:
self.ask_request()
if is_buy: if is_buy:
order_status = self.api.buylimit(exchange_symbol, amount, order_status = self.api.buylimit(exchange_symbol, amount,
price) price)
@@ -138,6 +141,7 @@ class Bittrex(Exchange):
def get_open_orders(self, asset): def get_open_orders(self, asset):
symbol = self.get_symbol(asset) symbol = self.get_symbol(asset)
try: try:
self.ask_request()
open_orders = self.api.getopenorders(symbol) open_orders = self.api.getopenorders(symbol)
except Exception as e: except Exception as e:
raise ExchangeRequestError(error=e) raise ExchangeRequestError(error=e)
@@ -181,6 +185,7 @@ class Bittrex(Exchange):
def get_order(self, order_id): def get_order(self, order_id):
log.info('retrieving order {}'.format(order_id)) log.info('retrieving order {}'.format(order_id))
try: try:
self.ask_request()
order_status = self.api.getorder(order_id) order_status = self.api.getorder(order_id)
except Exception as e: except Exception as e:
raise ExchangeRequestError(error=e) raise ExchangeRequestError(error=e)
@@ -196,6 +201,7 @@ class Bittrex(Exchange):
log.info('cancelling order {}'.format(order_id)) log.info('cancelling order {}'.format(order_id))
try: try:
self.ask_request()
status = self.api.cancel(order_id) status = self.api.cancel(order_id)
except Exception as e: except Exception as e:
raise ExchangeRequestError(error=e) raise ExchangeRequestError(error=e)
@@ -207,43 +213,59 @@ class Bittrex(Exchange):
error=status['message'] error=status['message']
) )
def get_candles(self, data_frequency, assets, bar_count=None): def get_candles(self, freq, assets, bar_count=None,
start_dt=None, end_dt=None):
""" """
Supported Intervals Supported Intervals
------------------- -------------------
day, oneMin, fiveMin, thirtyMin, hour day, oneMin, fiveMin, thirtyMin, hour
:param data_frequency: :param freq:
:param assets: :param assets:
:param bar_count: :param bar_count:
:param start_dt
:param end_dt
:return: :return:
""" """
log.info('retrieving candles')
if data_frequency == 'minute' or data_frequency == '1m': # 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' frequency = 'oneMin'
elif data_frequency == '5m': elif freq == '5T':
frequency = 'fiveMin' frequency = 'fiveMin'
elif data_frequency == '30m': elif freq == '30T':
frequency = 'thirtyMin' frequency = 'thirtyMin'
elif data_frequency == '1h': elif freq == '60T':
frequency = 'hour' frequency = 'hour'
elif data_frequency == 'daily' or data_frequency == '1D': elif freq == '1D':
frequency = 'day' frequency = 'day'
else: else:
raise InvalidHistoryFrequencyError( raise InvalidHistoryFrequencyError(frequency=freq)
frequency=data_frequency
)
# Making sure that assets are iterable # Making sure that assets are iterable
asset_list = [assets] if isinstance(assets, TradingPair) else assets asset_list = [assets] if isinstance(assets, TradingPair) else assets
ohlc_map = dict()
for asset in asset_list: for asset in asset_list:
end = int(time.mktime(end_dt.timetuple()))
url = '{url}/pub/market/GetTicks?marketName={symbol}' \ url = '{url}/pub/market/GetTicks?marketName={symbol}' \
'&tickInterval={frequency}&_=1499127220008'.format( '&tickInterval={frequency}&_={end}'.format(
url=URL2, url=URL2,
symbol=self.get_symbol(asset), symbol=self.get_symbol(asset),
frequency=frequency frequency=frequency,
end=end
) )
try: try:
@@ -271,9 +293,11 @@ class Bittrex(Exchange):
return ohlc return ohlc
ordered_candles = list(reversed(candles)) ordered_candles = list(reversed(candles))
ohlc_map = dict()
if bar_count is None: if bar_count is None:
ohlc_map[asset] = ohlc_from_candle(ordered_candles[0]) ohlc_map[asset] = ohlc_from_candle(ordered_candles[0])
else: else:
# TODO: optimize
ohlc_bars = [] ohlc_bars = []
for candle in ordered_candles[:bar_count]: for candle in ordered_candles[:bar_count]:
ohlc = ohlc_from_candle(candle) ohlc = ohlc_from_candle(candle)
@@ -298,6 +322,7 @@ class Bittrex(Exchange):
for asset in assets: for asset in assets:
symbol = self.get_symbol(asset) symbol = self.get_symbol(asset)
try: try:
self.ask_request()
ticker = self.api.getticker(symbol) ticker = self.api.getticker(symbol)
except Exception as e: except Exception as e:
raise ExchangeRequestError(error=e) raise ExchangeRequestError(error=e)
@@ -316,3 +341,76 @@ class Bittrex(Exchange):
def get_account(self): def get_account(self):
log.info('retrieving account data') log.info('retrieving account data')
pass 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
+9 -4
View File
@@ -3,11 +3,12 @@ import json
import time import time
import hmac import hmac
import hashlib import hashlib
import ssl
from six.moves import urllib
# Workaround for backwards compatibility # Workaround for backwards compatibility
# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7 # https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7
from six.moves import urllib
urlopen = urllib.request.urlopen urlopen = urllib.request.urlopen
@@ -39,13 +40,17 @@ class Bittrex_api(object):
if method not in self.public: if method not in self.public:
url += '&apikey=' + self.key url += '&apikey=' + self.key
url += '&nonce=' + str(int(time.time())) url += '&nonce=' + str(int(time.time()))
signature = hmac.new(self.secret, url, hashlib.sha512).hexdigest()
signature = hmac.new(self.secret.encode('utf-8'),
url.encode('utf-8'),
hashlib.sha512).hexdigest()
headers = {'apisign': signature} headers = {'apisign': signature}
else: else:
headers = {} headers = {}
req = urllib.request.Request(url, headers=headers) req = urllib.request.Request(url, headers=headers)
response = json.loads(urlopen(req).read()) response = json.loads(urlopen(
req, context=ssl._create_unverified_context()).read())
if response["result"]: if response["result"]:
return response["result"] return response["result"]
@@ -0,0 +1,7 @@
from catalyst.data.bundles import register
from catalyst.exchange.exchange_bundle import exchange_bundle
symbols = (
'neo_btc',
)
register('exchange_bitfinex', exchange_bundle('bitfinex', symbols))
+359
View File
@@ -0,0 +1,359 @@
import calendar
import os
import tarfile
from datetime import timedelta, datetime, date
import numpy as np
import pandas as pd
import pytz
from catalyst.assets._assets import TradingPair
from catalyst.data.bundles.core import download_without_progress
from catalyst.exchange.exchange_utils import get_exchange_bundles_folder, \
get_exchange_symbols
EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex']
API_URL = 'http://data.enigma.co/api/v1'
def get_date_from_ms(ms):
"""
The date from the number of miliseconds from the epoch.
Parameters
----------
ms: int
Returns
-------
datetime
"""
return datetime.fromtimestamp(ms / 1000.0)
def get_seconds_from_date(date):
"""
The number of seconds from the epoch.
Parameters
----------
date: datetime
Returns
-------
int
"""
epoch = datetime.utcfromtimestamp(0)
epoch = epoch.replace(tzinfo=pytz.UTC)
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):
"""
Get a time delta based on the specified data frequency.
Parameters
----------
periods: int
data_frequency: str
Returns
-------
timedelta
"""
return timedelta(minutes=periods) \
if data_frequency == 'minute' else timedelta(days=periods)
def get_periods_range(start_dt, end_dt, freq):
"""
Get a date range for the specified parameters.
Parameters
----------
start_dt: datetime
end_dt: datetime
freq: str
Returns
-------
DateTimeIndex
"""
if freq == 'minute':
freq = 'T'
elif freq == 'daily':
freq = 'D'
return pd.date_range(start_dt, end_dt, freq=freq)
def get_periods(start_dt, end_dt, freq):
"""
The number of periods in the specified range.
Parameters
----------
start_dt: datetime
end_dt: datetime
freq: str
Returns
-------
int
"""
return len(get_periods_range(start_dt, end_dt, freq))
def get_start_dt(end_dt, bar_count, data_frequency, include_first=True):
"""
The start date based on specified end date and data frequency.
Parameters
----------
end_dt: datetime
bar_count: int
data_frequency: str
Returns
-------
datetime
"""
periods = bar_count
if periods > 1:
delta = get_delta(periods, data_frequency)
start_dt = end_dt - delta
if not include_first:
start_dt += get_delta(1, data_frequency)
else:
start_dt = end_dt
return start_dt
def get_period_label(dt, data_frequency):
"""
The period label for the specified date and frequency.
Parameters
----------
dt: datetime
data_frequency: str
Returns
-------
str
"""
return '{}-{:02d}'.format(dt.year, dt.month) if data_frequency == 'minute' \
else '{}'.format(dt.year)
def get_month_start_end(dt, first_day=None, last_day=None):
"""
The first and last day of the month for the specified date.
Parameters
----------
dt: datetime
first_day: datetime
last_day: datetime
Returns
-------
datetime, datetime
"""
month_range = calendar.monthrange(dt.year, dt.month)
if first_day:
month_start = first_day
else:
month_start = pd.to_datetime(datetime(
dt.year, dt.month, 1, 0, 0, 0, 0
), utc=True)
if last_day:
month_end = last_day
else:
month_end = pd.to_datetime(datetime(
dt.year, dt.month, month_range[1], 23, 59, 0, 0
), utc=True)
if month_end > pd.Timestamp.utcnow():
month_end = pd.Timestamp.utcnow().floor('1D')
return month_start, month_end
def get_year_start_end(dt, first_day=None, last_day=None):
"""
The first and last day of the year for the specified date.
Parameters
----------
dt: datetime
first_day: datetime
last_day: datetime
Returns
-------
datetime, datetime
"""
year_start = first_day if first_day \
else pd.to_datetime(date(dt.year, 1, 1), utc=True)
year_end = last_day if last_day \
else pd.to_datetime(date(dt.year, 12, 31), utc=True)
if year_end > pd.Timestamp.utcnow():
year_end = pd.Timestamp.utcnow().floor('1D')
return year_start, year_end
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 as e:
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
-121
View File
@@ -1,121 +0,0 @@
#
# 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 time import sleep
from logbook import Logger
from catalyst.data.data_portal import DataPortal
from catalyst.exchange.exchange_errors import (
ExchangeRequestError,
ExchangeBarDataError
)
log = Logger('DataPortalExchange')
class DataPortalExchange(DataPortal):
def __init__(self, exchange, *args, **kwargs):
self.exchange = exchange
# TODO: put somewhere accessible by each algo
self.retry_get_history_window = 5
self.retry_get_spot_value = 5
self.retry_delay = 5
super(DataPortalExchange, self).__init__(*args, **kwargs)
def _get_history_window(self,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill=True,
attempt_index=0):
try:
return self.exchange.get_history_window(
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill)
except ExchangeRequestError as e:
log.warn(
'get history attempt {}: {}'.format(attempt_index, e)
)
if attempt_index < self.retry_get_history_window:
sleep(self.retry_delay)
return self._get_history_window(assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill,
attempt_index + 1)
else:
raise ExchangeBarDataError(
data_type='history',
attempts=attempt_index,
error=e
)
def get_history_window(self,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill=True):
return self._get_history_window(assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill)
def _get_spot_value(self, assets, field, dt, data_frequency,
attempt_index=0):
try:
return self.exchange.get_spot_value(assets, field, dt,
data_frequency)
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):
return self._get_spot_value(assets, field, dt, data_frequency)
def get_adjusted_value(self, asset, field, dt,
perspective_dt,
data_frequency,
spot_value=None):
# TODO: does this pertain to cryptocurrencies?
raise NotImplementedError("get_adjusted_value is not implemented yet!")
+501 -134
View File
@@ -1,7 +1,6 @@
import abc import abc
import collections
import random
from abc import ABCMeta, abstractmethod, abstractproperty from abc import ABCMeta, abstractmethod, abstractproperty
from datetime import timedelta
from time import sleep from time import sleep
import numpy as np import numpy as np
@@ -9,20 +8,24 @@ import pandas as pd
from catalyst.assets._assets import TradingPair from catalyst.assets._assets import TradingPair
from logbook import Logger from logbook import Logger
from catalyst.constants import LOG_LEVEL
from catalyst.data.data_portal import BASE_FIELDS from catalyst.data.data_portal import BASE_FIELDS
from catalyst.errors import ( from catalyst.exchange.bundle_utils import get_start_dt, \
SymbolNotFound, get_delta, get_periods, get_periods_range
) from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \ from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
InvalidOrderStyle, BaseCurrencyNotFoundError InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange, \
PricingDataNotLoadedError, \
NoDataAvailableOnExchange, ExchangeSymbolsNotFound
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \ from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
ExchangeLimitOrder, ExchangeStopOrder ExchangeLimitOrder, ExchangeStopOrder
from catalyst.exchange.exchange_portfolio import ExchangePortfolio from catalyst.exchange.exchange_portfolio import ExchangePortfolio
from catalyst.exchange.exchange_utils import get_exchange_symbols from catalyst.exchange.exchange_utils import get_exchange_symbols, \
get_frequency, resample_history_df
from catalyst.finance.order import ORDER_STATUS from catalyst.finance.order import ORDER_STATUS
from catalyst.finance.transaction import Transaction from catalyst.finance.transaction import Transaction
log = Logger('Exchange') log = Logger('Exchange', level=LOG_LEVEL)
class Exchange: class Exchange:
@@ -30,13 +33,18 @@ class Exchange:
def __init__(self): def __init__(self):
self.name = None self.name = None
self.trading_pairs = None self.assets = dict()
self.assets = {} self.local_assets = dict()
self._portfolio = None self._portfolio = None
self.minute_writer = None self.minute_writer = None
self.minute_reader = None self.minute_reader = None
self.base_currency = None self.base_currency = None
self.num_candles_limit = None
self.max_requests_per_minute = None
self.request_cpt = None
self.bundle = ExchangeBundle(self.name)
@property @property
def positions(self): def positions(self):
return self.portfolio.positions return self.portfolio.positions
@@ -44,9 +52,11 @@ class Exchange:
@property @property
def portfolio(self): def portfolio(self):
""" """
Return the Portfolio The exchange portfolio
:return: Returns
-------
ExchangePortfolio
""" """
if self._portfolio is None: if self._portfolio is None:
self._portfolio = ExchangePortfolio( self._portfolio = ExchangePortfolio(
@@ -64,12 +74,74 @@ class Exchange:
def time_skew(self): def time_skew(self):
pass pass
def is_open(self, dt):
"""
Is the exchange open
Parameters
----------
dt: Timestamp
Returns
-------
bool
"""
# TODO: implement for each exchange.
return True
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.
Returns
-------
bool
"""
now = pd.Timestamp.utcnow()
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 + timedelta(minutes=1):
self.request_cpt = dict()
self.request_cpt[now] = 0
return True
if cpt >= self.max_requests_per_minute:
delta = now - cpt_date
sleep_period = 60 - delta.total_seconds()
sleep(sleep_period)
now = pd.Timestamp.utcnow()
self.request_cpt = dict()
self.request_cpt[now] = 0
return True
else:
self.request_cpt[cpt_date] += 1
def get_symbol(self, asset): def get_symbol(self, asset):
""" """
Get the exchange specific symbol of the given asset. The exchange specific symbol of the specified market.
Parameters
----------
asset: TradingPair
Returns
-------
str
:param asset: Asset
:return: symbol: str
""" """
symbol = None symbol = None
@@ -79,7 +151,7 @@ class Exchange:
if not symbol: if not symbol:
raise ValueError('Currency %s not supported by exchange %s' % raise ValueError('Currency %s not supported by exchange %s' %
(asset['symbol'], self.name)) (asset['symbol'], self.name.title()))
return symbol return symbol
@@ -87,55 +159,122 @@ class Exchange:
""" """
Get a list of symbols corresponding to each given asset. Get a list of symbols corresponding to each given asset.
:param assets: Asset[] Parameters
:return: ----------
assets: list[TradingPair]
Returns
-------
list[str]
""" """
symbols = [] symbols = []
for asset in assets: for asset in assets:
symbols.append(self.get_symbol(asset)) symbols.append(self.get_symbol(asset))
return symbols return symbols
def get_asset(self, symbol): def get_assets(self, symbols=None, data_frequency=None):
""" """
Find an Asset on the current exchange based on its Catalyst symbol The list of markets for the specified symbols.
:param symbol: the [target]_[base] currency pair symbol
:return: Asset
"""
asset = None
Parameters
----------
symbols: list[str]
Returns
-------
list[TradingPair]
"""
assets = []
if symbols is not None:
for symbol in symbols:
asset = self.get_asset(symbol, data_frequency)
assets.append(asset)
else:
for key in self.assets: for key in self.assets:
if not asset and self.assets[key].symbol.lower() == symbol.lower(): assets.append(self.assets[key])
asset = self.assets[key]
if not asset: return assets
raise SymbolNotFound(symbol=symbol)
def _find_asset(self, asset, symbol, data_frequency, is_local=False):
assets = self.assets if not is_local else self.local_assets
for key in assets:
has_data = (data_frequency == 'minute'
and assets[key].end_minute is not None) \
or (data_frequency == 'daily'
and assets[key].end_daily is not None)
if not asset and assets[key].symbol.lower() == symbol.lower() \
and (not data_frequency or has_data):
asset = assets[key]
return asset return asset
def fetch_symbol_map(self): def get_asset(self, symbol, data_frequency=None):
return get_exchange_symbols(self.name) """
The market for the specified symbol.
def load_assets(self): Parameters
----------
symbol: str
Returns
-------
TradingPair
"""
asset = None
log.debug('searching asset {} on the server'.format(symbol))
asset = self._find_asset(asset, symbol, data_frequency, False)
log.debug('asset {} not found on the server, searching local '
'assets'.format(symbol))
asset = self._find_asset(asset, symbol, data_frequency, True)
if not asset:
all_values = list(self.assets.values()) + \
list(self.local_assets.values())
supported_symbols = sorted([
asset.symbol for asset in all_values
])
raise SymbolNotFoundOnExchange(
symbol=symbol,
exchange=self.name.title(),
supported_symbols=supported_symbols
)
return asset
def fetch_symbol_map(self, is_local=False):
return get_exchange_symbols(self.name, is_local)
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
currency pair symbol. The universal symbol is contained in the currency pair symbol. The universal symbol is contained in the
'symbol' attribute of each asset. 'symbol' attribute of each asset.
Notes Notes
----- -----
The sid of each asset is calculated based on a numeric hash of the The sid of each asset is calculated based on a numeric hash of the
universal symbol. This simple approach avoids maintaining a mapping universal symbol. This simple approach avoids maintaining a mapping
of sids. of sids.
This method can be overridden if an exchange offers equivalent data This method can be omerridden if an exchange offers equivalent data
via its api. via its api.
"""
symbol_map = self.fetch_symbol_map() """
try:
symbol_map = self.fetch_symbol_map(is_local)
except ExchangeSymbolsNotFound:
return None
for exchange_symbol in symbol_map: for exchange_symbol in symbol_map:
asset = symbol_map[exchange_symbol] asset = symbol_map[exchange_symbol]
@@ -159,15 +298,37 @@ class Exchange:
else: else:
asset_name = None 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( trading_pair = TradingPair(
symbol=asset['symbol'], symbol=asset['symbol'],
exchange=self.name, exchange=self.name,
start_date=start_date, start_date=start_date,
end_date=end_date, end_date=end_date,
leverage=leverage, leverage=leverage,
asset_name=asset_name asset_name=asset_name,
min_trade_size=min_trade_size,
end_daily=end_daily,
end_minute=end_minute,
exchange_symbol=exchange_symbol
) )
if is_local:
self.local_assets[exchange_symbol] = trading_pair
else:
self.assets[exchange_symbol] = trading_pair self.assets[exchange_symbol] = trading_pair
def check_open_orders(self): def check_open_orders(self):
@@ -176,8 +337,10 @@ class Exchange:
For each executed order found, create a transaction and apply to the For each executed order found, create a transaction and apply to the
Portfolio. Portfolio.
:return: Returns
transactions: Transaction[] -------
list[Transaction]
""" """
transactions = list() transactions = list()
if self.portfolio.open_orders: if self.portfolio.open_orders:
@@ -247,35 +410,37 @@ class Exchange:
'1D', '7D', '14D', '1M' '1D', '7D', '14D', '1M'
""" """
if field not in BASE_FIELDS: if field not in BASE_FIELDS:
raise KeyError('Invalid column: ' + str(field)) raise KeyError('Invalid column: {}'.format(field))
if isinstance(assets, collections.Iterable): values = []
values = list()
for asset in assets: for asset in assets:
value = self.get_single_spot_value( value = self.get_single_spot_value(asset, field, data_frequency)
asset, field, data_frequency)
values.append(value) values.append(value)
return values return values
else:
return self.get_single_spot_value(
assets, field, data_frequency)
def get_single_spot_value(self, asset, field, data_frequency): def get_single_spot_value(self, asset, field, data_frequency):
""" """
Similar to 'get_spot_value' but for a single asset Similar to 'get_spot_value' but for a single asset
Note Notes
---- -----
We're writing each minute bar to disk using zipline's machinery. We're writing each minute bar to disk using zipline's machinery.
This is especially useful when running multiple algorithms This is especially useful when running multiple algorithms
concurrently. By using local data when possible, we try to reaching concurrently. By using local data when possible, we try to reaching
request limits on exchanges. request limits on exchanges.
:param asset: Parameters
:param field: ----------
:param data_frequency: asset: TradingPair
:return value: The spot value of the given asset / field field: str
data_frequency: str
Returns
-------
float
The spot value of the given asset / field
""" """
log.debug( log.debug(
'fetching spot value {field} for symbol {symbol}'.format( 'fetching spot value {field} for symbol {symbol}'.format(
@@ -284,64 +449,58 @@ class Exchange:
) )
) )
if field == 'price': freq = '1T' if data_frequency == 'minute' else '1D'
field = 'close' ohlc = self.get_candles(freq, asset)
# Don't use a timezone here
dt = pd.Timestamp.utcnow().floor('1 min')
value = None
if self.minute_reader is not None:
try:
# Slight delay to minimize the chances that multiple algos
# might try to hit the cache at the exact same time.
sleep_time = random.uniform(0.5, 0.8)
sleep(sleep_time)
# TODO: This does not always! Why is that? Open an issue with zipline.
# See: https://github.com/zipline-live/zipline/issues/26
value = self.minute_reader.get_value(
sid=asset.sid,
dt=dt,
field=field
)
except Exception as e:
log.warn('minute data not found: {}'.format(e))
if value is None or np.isnan(value):
ohlc = self.get_candles(data_frequency, asset)
if field not in ohlc: if field not in ohlc:
raise KeyError('Invalid column: %s' % field) raise KeyError('Invalid column: %s' % field)
if self.minute_writer is not None:
df = pd.DataFrame(
[ohlc],
index=pd.DatetimeIndex([dt]),
columns=['open', 'high', 'low', 'close', 'volume']
)
try:
self.minute_writer.write_sid(
sid=asset.sid,
df=df
)
log.debug('wrote minute data: {}'.format(dt))
except Exception as e:
log.warn(
'unable to write minute data: {} {}'.format(dt, e))
value = ohlc[field] value = ohlc[field]
log.debug('got spot value: {}'.format(value)) log.debug('got spot value: {}'.format(value))
else:
log.debug('got spot value from cache: {}'.format(value))
return value return value
def get_series_from_candles(self, candles, start_dt, end_dt,
data_frequency, field, previous_value=None):
"""
Get a series of field data for the specified candles.
Parameters
----------
candles: list[dict[str, float]]
start_dt: datetime
end_dt: datetime
data_frequency: str
field: str
previous_value: float
Returns
-------
Series
"""
dates = [candle['last_traded'] for candle in candles]
values = [candle[field] for candle in candles]
series = pd.Series(values, index=dates)
periods = get_periods_range(
start_dt, end_dt, data_frequency
)
# TODO: ensure that this working as expected, if not use fillna
series = series.reindex(
periods,
method='ffill',
fill_value=previous_value,
)
return series
def get_history_window(self, def get_history_window(self,
assets, assets,
end_dt, end_dt,
bar_count, bar_count,
frequency, frequency,
field, field,
data_frequency, data_frequency=None,
ffill=True): ffill=True):
""" """
@@ -350,10 +509,11 @@ class Exchange:
Parameters Parameters
---------- ----------
assets : list of catalyst.data.Asset objects assets : list[TradingPair]
The assets whose data is desired. The assets whose data is desired.
end_dt: not applicable to cryptocurrencies end_dt: datetime
The date of the last bar
bar_count: int bar_count: int
The number of bars desired. The number of bars desired.
@@ -375,26 +535,151 @@ class Exchange:
Returns Returns
------- -------
DataFrame
A dataframe containing the requested data. A dataframe containing the requested data.
"""
"""
freq, candle_size, unit, data_frequency = get_frequency(
frequency, data_frequency
)
adj_bar_count = candle_size * bar_count
start_dt = get_start_dt(end_dt, adj_bar_count, data_frequency)
# The get_history method supports multiple asset
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
) )
series = dict() series = dict()
for asset in candles:
asset_series = self.get_series_from_candles(
candles=candles[asset],
start_dt=start_dt,
end_dt=end_dt,
data_frequency=frequency,
field=field,
)
series[asset] = asset_series
df = pd.DataFrame(series)
df.dropna(inplace=True)
return df
def get_history_window_with_bundle(self,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency=None,
ffill=True,
force_auto_ingest=False):
"""
Public API method that returns a dataframe containing the requested
history window. Data is fully adjusted.
Parameters
----------
assets : list[TradingPair]
The assets whose data is desired.
end_dt: datetime
The date of the last bar.
bar_count: int
The number of bars desired.
frequency: string
"1d" or "1m"
field: string
The desired field of the asset.
data_frequency: string
The frequency of the data to query; i.e. whether the data is
'daily' or 'minute' bars.
# TODO: fill how?
ffill: boolean
Forward-fill missing values. Only has effect if field
is 'price'.
Returns
-------
DataFrame
A dataframe containing the requested data.
"""
freq, candle_size, unit, data_frequency = get_frequency(
frequency, data_frequency
)
adj_bar_count = candle_size * bar_count
try:
series = self.bundle.get_history_window_series_and_load(
assets=assets,
end_dt=end_dt,
bar_count=adj_bar_count,
field=field,
data_frequency=data_frequency,
force_auto_ingest=force_auto_ingest
)
except (PricingDataNotLoadedError, NoDataAvailableOnExchange):
series = dict()
for asset in assets: for asset in assets:
asset_candles = candles[asset] if asset not in series or series[asset].index[-1] < end_dt:
# Adding bars too recent to be contained in the consolidated
# exchanges bundles. We go directly against the exchange
# to retrieve the candles.
start_dt = get_start_dt(end_dt, adj_bar_count, data_frequency)
trailing_dt = \
series[asset].index[-1] + get_delta(1, data_frequency) \
if asset in series else start_dt
values = map(lambda candle: candle[field], asset_candles) # The get_history method supports multiple asset
dates = map(lambda candle: candle['last_traded'], asset_candles) # Use the original frequency to let each api optimize
# the size of result sets
trailing_bar_count = get_periods(
trailing_dt, end_dt, freq
)
candles = self.get_candles(
freq=freq,
assets=asset,
bar_count=trailing_bar_count,
start_dt=start_dt,
end_dt=end_dt
)
value_series = pd.Series(values, index=dates) last_value = series[asset].iloc(0) if asset in series \
series[asset] = value_series else np.nan
# Create a series with the common data_frequency, ffill
# missing values
candle_series = self.get_series_from_candles(
candles=candles,
start_dt=trailing_dt,
end_dt=end_dt,
data_frequency=data_frequency,
field=field,
previous_value=last_value
)
if asset in series:
series[asset].append(candle_series)
else:
series[asset] = candle_series
df = resample_history_df(pd.DataFrame(series), freq, field)
# TODO: consider this more carefully
df.dropna(inplace=True)
df = pd.concat(series)
return df return df
def synchronize_portfolio(self): def synchronize_portfolio(self):
@@ -402,7 +687,6 @@ class Exchange:
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.
:return:
""" """
log.debug('synchronizing portfolio with exchange {}'.format(self.name)) log.debug('synchronizing portfolio with exchange {}'.format(self.name))
balances = self.get_balances() balances = self.get_balances()
@@ -413,7 +697,7 @@ class Exchange:
if base_position_available is None: if base_position_available is None:
raise BaseCurrencyNotFoundError( raise BaseCurrencyNotFoundError(
base_currency=self.base_currency, base_currency=self.base_currency,
exchange=self.name exchange=self.name.title()
) )
portfolio = self._portfolio portfolio = self._portfolio
@@ -424,7 +708,7 @@ class Exchange:
portfolio.starting_cash = portfolio.cash portfolio.starting_cash = portfolio.cash
if portfolio.positions: if portfolio.positions:
assets = portfolio.positions.keys() assets = list(portfolio.positions.keys())
tickers = self.tickers(assets) tickers = self.tickers(assets)
portfolio.positions_value = 0.0 portfolio.positions_value = 0.0
@@ -440,34 +724,26 @@ class Exchange:
portfolio.portfolio_value = \ portfolio.portfolio_value = \
portfolio.positions_value + portfolio.cash portfolio.positions_value + portfolio.cash
@abstractmethod
def get_balances(self):
"""
Retrieve wallet balances for the exchange
:return balances: A dict of currency => available balance
"""
pass
@abstractmethod
def create_order(self, asset, amount, is_buy, style):
pass
def order(self, asset, amount, limit_price=None, stop_price=None, def order(self, asset, amount, limit_price=None, stop_price=None,
style=None): style=None):
"""Place an order. """Place an order.
Parameters Parameters
---------- ----------
asset : Asset asset : TradingPair
The asset that this order is for. The asset that this order is for.
amount : int amount : int
The amount of shares to order. If ``amount`` is positive, this is The amount of shares to order. If ``amount`` is positive, this is
the number of shares to buy or cover. If ``amount`` is negative, the number of shares to buy or cover. If ``amount`` is negative,
this is the number of shares to sell or short. this is the number of shares to sell or short.
limit_price : float, optional limit_price : float, optional
The limit price for the order. The limit price for the order.
stop_price : float, optional stop_price : float, optional
The stop price for the order. The stop price for the order.
style : ExecutionStyle, optional style : ExecutionStyle, optional
The execution style for the order. The execution style for the order.
@@ -492,6 +768,7 @@ class Exchange:
:class:`catalyst.finance.execution.ExecutionStyle` :class:`catalyst.finance.execution.ExecutionStyle`
:func:`catalyst.api.order_value` :func:`catalyst.api.order_value`
:func:`catalyst.api.order_percent` :func:`catalyst.api.order_percent`
""" """
if amount == 0: if amount == 0:
log.warn('skipping order amount of 0') log.warn('skipping order amount of 0')
@@ -515,7 +792,7 @@ class Exchange:
style = ExchangeStopOrder(stop_price, exchange=self.name) style = ExchangeStopOrder(stop_price, exchange=self.name)
elif style is not None: elif style is not None:
raise InvalidOrderStyle(exchange=self.name, raise InvalidOrderStyle(exchange=self.name.title(),
style=style.__class__.__name__) style=style.__class__.__name__)
else: else:
raise ValueError('Incomplete order data.') raise ValueError('Incomplete order data.')
@@ -537,6 +814,46 @@ class Exchange:
else: else:
return None return None
# The methods below must be implemented for each exchange.
@abstractmethod
def get_balances(self):
"""
Retrieve wallet balances for the exchange.
Returns
-------
dict[TradingPair, float]
"""
pass
@abstractmethod
def create_order(self, asset, amount, is_buy, style):
"""
Place an order on the exchange.
Parameters
----------
asset: TradingPair
The target market.
amount: float
The amount of shares to order. If ``amount`` is positive, this is
the number of shares to buy or cover. If ``amount`` is negative,
this is the number of shares to sell or short.
is_buy: bool
Is it a buy order?
style: ExecutionStyle
Returns
-------
Order
"""
pass
@abstractmethod @abstractmethod
def get_open_orders(self, asset): def get_open_orders(self, asset):
"""Retrieve all of the current open orders. """Retrieve all of the current open orders.
@@ -588,16 +905,43 @@ class Exchange:
pass pass
@abstractmethod @abstractmethod
def get_candles(self, data_frequency, assets, bar_count=None): def get_candles(self, freq, assets, bar_count=None,
start_dt=None, end_dt=None):
""" """
Retrieve OHLCV candles for the given assets Retrieve OHLCV candles for the given assets
:param data_frequency: Parameters
:param assets: ----------
:param end_dt: freq: str
:param bar_count: The frequency alias per convention:
:param limit: http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
:return:
assets: list[TradingPair]
The targeted assets.
bar_count: int
The number of bar desired. (default 1)
end_dt: datetime, optional
The last bar date.
start_dt: datetime, optional
The first bar date.
Returns
-------
dict[TradingPair, dict[str, Object]]
A dictionary of OHLCV candles. Each TradingPair instance is
mapped to a list of dictionaries with this structure:
open: float
high: float
low: float
close: float
volume: float
last_traded: datetime
See definition here:
http://www.investopedia.com/terms/o/ohlcchart.asp
""" """
pass pass
@@ -606,8 +950,14 @@ class Exchange:
""" """
Retrieve current tick data for the given assets Retrieve current tick data for the given assets
:param assets: Parameters
:return: ----------
assets: list[TradingPair]
Returns
-------
list[dict[str, float]
""" """
pass pass
@@ -615,6 +965,23 @@ class Exchange:
def get_account(self): def get_account(self):
""" """
Retrieve the account parameters. Retrieve the account parameters.
:return: """
pass
@abc.abstractmethod
def get_orderbook(self, asset, order_type, limit):
"""
Retrieve the the orderbook for the given trading pair.
Parameters
----------
asset: TradingPair
order_type: str
The type of orders: bid, ask or all
limit: int
Returns
-------
list[dict[str, float]
""" """
pass pass
@@ -10,44 +10,49 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import os import pickle
import signal import signal
import sys import sys
import pickle from collections import deque
from datetime import timedelta from datetime import timedelta
from time import sleep
from os import listdir from os import listdir
from os.path import isfile, join from os.path import isfile, join
from collections import deque from time import sleep
import numpy as np
import logbook import logbook
import pandas as pd import pandas as pd
from catalyst.assets._assets import TradingPair
import catalyst.protocol as zp import catalyst.protocol as zp
from catalyst.algorithm import TradingAlgorithm from catalyst.algorithm import TradingAlgorithm
from catalyst.data.minute_bars import BcolzMinuteBarWriter, \ from catalyst.constants import LOG_LEVEL
BcolzMinuteBarReader
from catalyst.errors import OrderInBeforeTradingStart from catalyst.errors import OrderInBeforeTradingStart
from catalyst.exchange.simple_clock import SimpleClock from catalyst.exchange.exchange_blotter import ExchangeBlotter
from catalyst.exchange.live_graph_clock import LiveGraphClock
from catalyst.exchange.exchange_errors import ( from catalyst.exchange.exchange_errors import (
ExchangeRequestError, ExchangeRequestError,
ExchangePortfolioDataError, ExchangePortfolioDataError,
ExchangeTransactionError ExchangeTransactionError,
) OrphanOrderError)
from catalyst.exchange.exchange_utils import get_exchange_minute_writer_root, \ from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
save_algo_object, get_algo_object, get_algo_folder, get_algo_df, \ ExchangeLimitOrder, ExchangeStopOrder
from catalyst.exchange.exchange_utils import save_algo_object, get_algo_object, \
get_algo_folder, get_algo_df, \
save_algo_df save_algo_df
from catalyst.exchange.live_graph_clock import LiveGraphClock
from catalyst.exchange.simple_clock import SimpleClock
from catalyst.exchange.stats_utils import get_pretty_stats from catalyst.exchange.stats_utils import get_pretty_stats
from catalyst.finance.execution import MarketOrder
from catalyst.finance.performance.period import calc_period_stats from catalyst.finance.performance.period import calc_period_stats
from catalyst.gens.tradesimulation import AlgorithmSimulator from catalyst.gens.tradesimulation import AlgorithmSimulator
from catalyst.utils.api_support import ( from catalyst.utils.api_support import (
api_method, api_method,
disallowed_in_before_trading_start) disallowed_in_before_trading_start)
from catalyst.utils.input_validation import error_keywords from catalyst.utils.input_validation import error_keywords, ensure_upper_case, \
expect_types
from catalyst.utils.math_utils import round_nearest
from catalyst.utils.preprocess import preprocess
log = logbook.Logger("ExchangeTradingAlgorithm") log = logbook.Logger('exchange_algorithm', level=LOG_LEVEL)
class ExchangeAlgorithmExecutor(AlgorithmSimulator): class ExchangeAlgorithmExecutor(AlgorithmSimulator):
@@ -55,14 +60,238 @@ class ExchangeAlgorithmExecutor(AlgorithmSimulator):
super(self.__class__, self).__init__(*args, **kwargs) super(self.__class__, self).__init__(*args, **kwargs)
class ExchangeTradingAlgorithm(TradingAlgorithm): class ExchangeTradingAlgorithmBase(TradingAlgorithm):
def __init__(self, *args, **kwargs):
self.exchanges = kwargs.pop('exchanges', None)
super(ExchangeTradingAlgorithmBase, self).__init__(*args, **kwargs)
def round_order(self, amount, asset):
"""
We need fractions with cryptocurrencies
:param amount:
:return:
"""
return round_nearest(amount, asset.min_trade_size)
@api_method
@preprocess(symbol_str=ensure_upper_case)
def symbol(self, symbol_str, exchange_name=None):
"""Lookup an Equity by its ticker symbol.
Parameters
----------
symbol_str : str
The ticker symbol for the equity to lookup.
exchange_name: str
The name of the exchange containing the symbol
Returns
-------
equity : Equity
The equity that held the ticker symbol on the current
symbol lookup date.
Raises
------
SymbolNotFound
Raised when the symbols was not held on the current lookup date.
See Also
--------
:func:`catalyst.api.set_symbol_lookup_date`
"""
# If the user has not set the symbol lookup date,
# use the end_session as the date for sybmol->sid resolution.
_lookup_date = self._symbol_lookup_date \
if self._symbol_lookup_date is not None \
else self.sim_params.end_session
if exchange_name is None:
exchange = list(self.exchanges.values())[0]
else:
exchange = self.exchanges[exchange_name]
data_frequency = self.data_frequency \
if self.sim_params.arena == 'backtest' else None
return self.asset_finder.lookup_symbol(
symbol=symbol_str,
exchange=exchange,
data_frequency=data_frequency,
as_of_date=_lookup_date
)
def prepare_period_stats(self, start_dt, end_dt):
"""
Creates a dictionary representing the state of the tracker.
Parameters
----------
start_dt: datetime
end_dt: datetime
Notes
-----
I rewrote this in an attempt to better control the stats.
I don't want things to happen magically through complex logic
pertaining to backtesting.
"""
tracker = self.perf_tracker
period = tracker.todays_performance
pos_stats = period.position_tracker.stats()
period_stats = calc_period_stats(pos_stats, period.ending_cash)
stats = dict(
period_start=tracker.period_start,
period_end=tracker.period_end,
capital_base=tracker.capital_base,
progress=tracker.progress,
ending_value=period.ending_value,
ending_exposure=period.ending_exposure,
capital_used=period.cash_flow,
starting_value=period.starting_value,
starting_exposure=period.starting_exposure,
starting_cash=period.starting_cash,
ending_cash=period.ending_cash,
portfolio_value=period.ending_cash + period.ending_value,
pnl=period.pnl,
returns=period.returns,
period_open=period.period_open,
period_close=period.period_close,
gross_leverage=period_stats.gross_leverage,
net_leverage=period_stats.net_leverage,
short_exposure=pos_stats.short_exposure,
long_exposure=pos_stats.long_exposure,
short_value=pos_stats.short_value,
long_value=pos_stats.long_value,
longs_count=pos_stats.longs_count,
shorts_count=pos_stats.shorts_count,
)
# Merging cumulative risk
stats.update(tracker.cumulative_risk_metrics.to_dict())
# Merging latest recorded variables
stats.update(self.recorded_vars)
stats['positions'] = period.position_tracker.get_positions_list()
# we want the key to be absent, not just empty
# Only include transactions for given dt
stats['transactions'] = []
for date in period.processed_transactions:
if start_dt <= date < end_dt:
transactions = period.processed_transactions[date]
for t in transactions:
stats['transactions'].append(t.to_dict())
stats['orders'] = []
for date in period.orders_by_modified:
if start_dt <= date < end_dt:
orders = period.orders_by_modified[date]
for order in orders:
stats['orders'].append(orders[order].to_dict())
return stats
class ExchangeTradingAlgorithmBacktest(ExchangeTradingAlgorithmBase):
def __init__(self, *args, **kwargs):
super(ExchangeTradingAlgorithmBacktest, self).__init__(*args, **kwargs)
self.frame_stats = list()
self.blotter = ExchangeBlotter(
data_frequency=self.data_frequency,
# Default to NeverCancel in catalyst
cancel_policy=self.cancel_policy,
)
log.info('initialized trading algorithm in backtest mode')
def _calculate_order(self, asset, amount,
limit_price=None, stop_price=None, style=None):
# Raises a ZiplineError if invalid parameters are detected.
self.validate_order_params(asset,
amount,
limit_price,
stop_price,
style)
# Convert deprecated limit_price and stop_price parameters to use
# ExecutionStyle objects.
style = self.__convert_order_params_for_blotter(limit_price,
stop_price,
style)
return amount, style
@staticmethod
def __convert_order_params_for_blotter(limit_price, stop_price, style):
"""
Helper method for converting deprecated limit_price and stop_price
arguments into ExecutionStyle instances.
This function assumes that either style == None or (limit_price,
stop_price) == (None, None).
"""
if style:
assert (limit_price, stop_price) == (None, None)
return style
if limit_price and stop_price:
return ExchangeStopLimitOrder(limit_price, stop_price)
if limit_price:
return ExchangeLimitOrder(limit_price)
if stop_price:
return ExchangeStopOrder(stop_price)
else:
return MarketOrder()
def is_last_frame_of_day(self, data):
# TODO: adjust here to support more intervals
next_frame_dt = data.current_dt + timedelta(minutes=1)
if next_frame_dt.date() > data.current_dt.date():
return True
else:
return False
def handle_data(self, data):
super(ExchangeTradingAlgorithmBacktest, self).handle_data(data)
if self.data_frequency == 'minute':
frame_stats = self.prepare_period_stats(
data.current_dt, data.current_dt + timedelta(minutes=1)
)
self.frame_stats.append(frame_stats)
def _create_stats_df(self):
stats = pd.DataFrame(self.frame_stats)
stats.set_index('period_close', inplace=True, drop=False)
return stats
def analyze(self, perf):
stats = self._create_stats_df() if self.data_frequency == 'minute' \
else perf
super(ExchangeTradingAlgorithmBacktest, self).analyze(stats)
def run(self, data=None, overwrite_sim_params=True):
perf = super(ExchangeTradingAlgorithmBacktest, self).run(
data, overwrite_sim_params
)
# Rebuilding the stats to support minute data
stats = self._create_stats_df() if self.data_frequency == 'minute' \
else perf
return stats
class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.exchange = kwargs.pop('exchange', None)
self.algo_namespace = kwargs.pop('algo_namespace', None) self.algo_namespace = kwargs.pop('algo_namespace', None)
self.live_graph = kwargs.pop('live_graph', None) self.live_graph = kwargs.pop('live_graph', None)
self._clock = None self._clock = None
self.minute_stats = deque(maxlen=60) self.frame_stats = deque(maxlen=60)
self.pnl_stats = get_algo_df(self.algo_namespace, 'pnl_stats') self.pnl_stats = get_algo_df(self.algo_namespace, 'pnl_stats')
@@ -82,34 +311,25 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
self.stats_minutes = 5 self.stats_minutes = 5
super(self.__class__, self).__init__(*args, **kwargs) super(ExchangeTradingAlgorithmLive, self).__init__(*args, **kwargs)
# self._create_minute_writer()
signal.signal(signal.SIGINT, self.signal_handler) signal.signal(signal.SIGINT, self.signal_handler)
log.info('exchange trading algorithm successfully initialized') log.info('initialized trading algorithm in live mode')
def _create_minute_writer(self):
root = get_exchange_minute_writer_root(self.exchange.name)
filename = os.path.join(root, 'metadata.json')
if os.path.isfile(filename):
writer = BcolzMinuteBarWriter.open(
root, self.sim_params.end_session)
else:
writer = BcolzMinuteBarWriter(
rootdir=root,
calendar=self.trading_calendar,
minutes_per_day=1440,
start_session=self.sim_params.start_session,
end_session=self.sim_params.end_session,
write_metadata=True
)
self.exchange.minute_writer = writer
self.exchange.minute_reader = BcolzMinuteBarReader(root)
def signal_handler(self, signal, frame): def signal_handler(self, signal, frame):
"""
Handles the keyboard interruption signal.
Parameters
----------
signal
frame
Returns
-------
"""
self.is_running = False self.is_running = False
if self._analyze is None: if self._analyze is None:
@@ -163,13 +383,11 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
if self.live_graph: if self.live_graph:
self._clock = LiveGraphClock( self._clock = LiveGraphClock(
self.sim_params.sessions, self.sim_params.sessions,
time_skew=self.exchange.time_skew,
context=self context=self
) )
else: else:
self._clock = SimpleClock( self._clock = SimpleClock(
self.sim_params.sessions, self.sim_params.sessions,
time_skew=self.exchange.time_skew
) )
return self._clock return self._clock
@@ -200,24 +418,32 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
""" """
We skip the entire performance tracker business and update the We skip the entire performance tracker business and update the
portfolio directly. portfolio directly.
:return:
Returns
-------
ExchangePortfolio
""" """
return self.exchange.portfolio # TODO: build cumulative portfolio
return self.perf_tracker.get_portfolio(False)
def updated_account(self): def updated_account(self):
return self.exchange.account return self.perf_tracker.get_account(False)
def _synchronize_portfolio(self, attempt_index=0): def _synchronize_portfolio(self, attempt_index=0):
try: try:
self.exchange.synchronize_portfolio() for exchange_name in self.exchanges:
exchange = self.exchanges[exchange_name]
exchange.synchronize_portfolio()
# Applying the updated last_sales_price to the positions # Applying the updated last_sales_price to the positions
# in the performance tracker. This seems a bit redundant # in the performance tracker. This seems a bit redundant
# but it will make sense when we have multiple exchange portfolios # but it will make sense when we have multiple exchange portfolios
# feeding into the same performance tracker. # feeding into the same performance tracker.
tracker = self.perf_tracker.todays_performance.position_tracker tracker = self.perf_tracker.todays_performance.position_tracker
for asset in self.exchange.portfolio.positions: for asset in exchange.portfolio.positions:
position = self.exchange.portfolio.positions[asset] position = exchange.portfolio.positions[asset]
tracker.update_position( tracker.update_position(
asset=asset, asset=asset,
last_sale_date=position.last_sale_date, last_sale_date=position.last_sale_date,
@@ -239,7 +465,14 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
def _check_open_orders(self, attempt_index=0): def _check_open_orders(self, attempt_index=0):
try: try:
return self.exchange.check_open_orders() orders = list()
for exchange_name in self.exchanges:
exchange = self.exchanges[exchange_name]
exchange_orders = exchange.check_open_orders()
orders += exchange_orders
return orders
except ExchangeRequestError as e: except ExchangeRequestError as e:
log.warn( log.warn(
'check open orders attempt {}: {}'.format(attempt_index, e) 'check open orders attempt {}: {}'.format(attempt_index, e)
@@ -255,6 +488,17 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
) )
def add_pnl_stats(self, period_stats): def add_pnl_stats(self, period_stats):
"""
Save p&l stats.
Parameters
----------
period_stats
Returns
-------
"""
starting = period_stats['starting_cash'] starting = period_stats['starting_cash']
current = period_stats['portfolio_value'] current = period_stats['portfolio_value']
appreciation = (current / starting) - 1 appreciation = (current / starting) - 1
@@ -271,6 +515,17 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
save_algo_df(self.algo_namespace, 'pnl_stats', self.pnl_stats) save_algo_df(self.algo_namespace, 'pnl_stats', self.pnl_stats)
def add_custom_signals_stats(self, period_stats): def add_custom_signals_stats(self, period_stats):
"""
Save custom signals stats.
Parameters
----------
period_stats
Returns
-------
"""
log.debug('adding custom signals stats: {}'.format(self.recorded_vars)) log.debug('adding custom signals stats: {}'.format(self.recorded_vars))
df = pd.DataFrame( df = pd.DataFrame(
data=[self.recorded_vars], data=[self.recorded_vars],
@@ -282,6 +537,17 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
self.custom_signals_stats) self.custom_signals_stats)
def add_exposure_stats(self, period_stats): def add_exposure_stats(self, period_stats):
"""
Save exposure stats.
Parameters
----------
period_stats
Returns
-------
"""
data = dict( data = dict(
long_exposure=period_stats['long_exposure'], long_exposure=period_stats['long_exposure'],
base_currency=period_stats['ending_cash'] base_currency=period_stats['ending_cash']
@@ -294,86 +560,31 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
) )
self.exposure_stats = pd.concat([self.exposure_stats, df]) self.exposure_stats = pd.concat([self.exposure_stats, df])
save_algo_df(self.algo_namespace, 'exposure_stats', save_algo_df(
self.exposure_stats) self.algo_namespace, 'exposure_stats', self.exposure_stats
def prepare_period_stats(self, start_dt, end_dt):
"""
Creates a dictionary representing the state of the tracker.
I rewrote this in an attempt to better control the stats.
I don't want things to happen magically through complex logic
pertaining to backtesting.
"""
tracker = self.perf_tracker
period = tracker.todays_performance
pos_stats = period.position_tracker.stats()
period_stats = calc_period_stats(pos_stats, period.ending_cash)
stats = dict(
period_start=tracker.period_start,
period_end=tracker.period_end,
capital_base=tracker.capital_base,
progress=tracker.progress,
ending_value=period.ending_value,
ending_exposure=period.ending_exposure,
capital_used=period.cash_flow,
starting_value=period.starting_value,
starting_exposure=period.starting_exposure,
starting_cash=period.starting_cash,
ending_cash=period.ending_cash,
portfolio_value=period.ending_cash + period.ending_value,
pnl=period.pnl,
returns=period.returns,
period_open=period.period_open,
period_close=period.period_close,
gross_leverage=period_stats.gross_leverage,
net_leverage=period_stats.net_leverage,
short_exposure=pos_stats.short_exposure,
long_exposure=pos_stats.long_exposure,
short_value=pos_stats.short_value,
long_value=pos_stats.long_value,
longs_count=pos_stats.longs_count,
shorts_count=pos_stats.shorts_count,
) )
# Merging cumulative risk
stats.update(tracker.cumulative_risk_metrics.to_dict())
# Merging latest recorded variables
stats.update(self.recorded_vars)
stats['positions'] = period.position_tracker.get_positions_list()
# we want the key to be absent, not just empty
# Only include transactions for given dt
stats['transactions'] = dict()
for date in period.processed_transactions:
if start_dt <= date < end_dt:
stats['transactions'][date] = \
period.processed_transactions[date]
stats['orders'] = dict()
for date in period.orders_by_modified:
if start_dt <= date < end_dt:
stats['orders'][date] = \
period.orders_by_modified[date]
return stats
def handle_data(self, data): def handle_data(self, data):
"""
Wrapper around the handle_data method of each algo.
Parameters
----------
data
"""
if not self.is_running: if not self.is_running:
return return
self._synchronize_portfolio() self._synchronize_portfolio()
transactions = self._check_open_orders() transactions = self._check_open_orders()
if len(transactions) > 0:
for transaction in transactions: for transaction in transactions:
self.perf_tracker.process_transaction(transaction) self.perf_tracker.process_transaction(transaction)
self.perf_tracker.update_performance()
if self._handle_data: if self._handle_data:
self._handle_data(self, data) self._handle_data(self, data)
@@ -387,21 +598,30 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
# Performance tracker and keep only minute and cumulative # Performance tracker and keep only minute and cumulative
self.perf_tracker.update_performance() self.perf_tracker.update_performance()
minute_stats = self.prepare_period_stats( frame_stats = self.prepare_period_stats(
data.current_dt, data.current_dt + timedelta(minutes=1)) data.current_dt, data.current_dt + timedelta(minutes=1))
# Saving the last hour in memory # Saving the last hour in memory
self.minute_stats.append(minute_stats) self.frame_stats.append(frame_stats)
self.add_pnl_stats(minute_stats) self.add_pnl_stats(frame_stats)
self.add_custom_signals_stats(minute_stats) if self.recorded_vars:
self.add_exposure_stats(minute_stats) self.add_custom_signals_stats(frame_stats)
recorded_cols = list(self.recorded_vars.keys())
else:
recorded_cols = None
print_df = pd.DataFrame(list(self.minute_stats)) self.add_exposure_stats(frame_stats)
log.debug(
print_df = pd.DataFrame(list(self.frame_stats))
log.info(
'statistics for the last {stats_minutes} minutes:\n{stats}'.format( 'statistics for the last {stats_minutes} minutes:\n{stats}'.format(
stats_minutes=self.stats_minutes, stats_minutes=self.stats_minutes,
stats=get_pretty_stats(print_df, self.stats_minutes) stats=get_pretty_stats(
stats_df=print_df,
recorded_cols=recorded_cols,
num_rows=self.stats_minutes
)
)) ))
today = pd.to_datetime('today', utc=True) today = pd.to_datetime('today', utc=True)
@@ -419,6 +639,7 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
except Exception as e: except Exception as e:
log.warn('unable to calculate performance: {}'.format(e)) log.warn('unable to calculate performance: {}'.format(e))
# TODO: pickle does not seem to work in python 3
try: try:
save_algo_object( save_algo_object(
algo_name=self.algo_namespace, algo_name=self.algo_namespace,
@@ -429,10 +650,12 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
log.warn('unable to save minute perfs to disk: {}'.format(e)) log.warn('unable to save minute perfs to disk: {}'.format(e))
try: try:
for exchange_name in self.exchanges:
exchange = self.exchanges[exchange_name]
save_algo_object( save_algo_object(
algo_name=self.algo_namespace, algo_name=self.algo_namespace,
key='portfolio_{}'.format(self.exchange.name), key='portfolio_{}'.format(exchange_name),
obj=self.exchange.portfolio obj=exchange.portfolio
) )
except Exception as e: except Exception as e:
log.warn('unable to save portfolio to disk: {}'.format(e)) log.warn('unable to save portfolio to disk: {}'.format(e))
@@ -445,7 +668,8 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
style=None, style=None,
attempt_index=0): attempt_index=0):
try: try:
return self.exchange.order(asset, amount, limit_price, exchange = self.exchanges[asset.exchange]
return exchange.order(asset, amount, limit_price,
stop_price, stop_price,
style) style)
except ExchangeRequestError as e: except ExchangeRequestError as e:
@@ -466,41 +690,71 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
@api_method @api_method
@disallowed_in_before_trading_start(OrderInBeforeTradingStart()) @disallowed_in_before_trading_start(OrderInBeforeTradingStart())
@expect_types(asset=TradingPair)
def order(self, def order(self,
asset, asset,
amount, amount,
limit_price=None, limit_price=None,
stop_price=None, stop_price=None,
style=None): style=None):
"""
We use the exchange specific portfolio to place orders.
The cumulative portfolio does not contain open orders but exchange
portfolios do.
Parameters
----------
asset: TradingPair
amount: float
limit_price: float
stop_price: float
style: Style
order: Order
The catalyst order object or None
"""
amount, style = self._calculate_order(asset, amount, amount, style = self._calculate_order(asset, amount,
limit_price, stop_price, limit_price, stop_price,
style) style)
order_id = self._order(asset, amount, limit_price, stop_price, style) order_id = self._order(asset, amount, limit_price, stop_price, style)
exchange = self.exchanges[asset.exchange]
exchange_portfolio = exchange.portfolio
if order_id is not None: if order_id is not None:
order = self.portfolio.open_orders[order_id]
if order_id in exchange_portfolio.open_orders:
order = exchange_portfolio.open_orders[order_id]
self.perf_tracker.process_order(order) self.perf_tracker.process_order(order)
return order return order
else: else:
raise OrphanOrderError(
order_id=order_id,
exchange=exchange.name
)
else:
log.warn('unable to order {} {} on exchange {}'.format(
amount, asset.symbol, asset.exchange))
return None return None
def round_order(self, amount):
"""
We need fractions with cryptocurrencies
:param amount:
:return:
"""
return amount
@api_method @api_method
def batch_market_order(self, share_counts): def batch_market_order(self, share_counts):
raise NotImplementedError() raise NotImplementedError()
def _get_open_orders(self, asset=None, attempt_index=0): def _get_open_orders(self, asset=None, attempt_index=0):
try: try:
return self.exchange.get_open_orders(asset) if asset:
exchange = self.exchanges[asset.exchange]
return exchange.get_open_orders(asset)
else:
open_orders = []
for exchange_name in self.exchanges:
exchange = self.exchanges[exchange_name]
exchange_orders = exchange.get_open_orders()
open_orders.append(exchange_orders)
return open_orders
except ExchangeRequestError as e: except ExchangeRequestError as e:
log.warn( log.warn(
'open orders attempt {}: {}'.format(attempt_index, e) 'open orders attempt {}: {}'.format(attempt_index, e)
@@ -519,15 +773,57 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
'get_open_orders. Use `asset` instead.') 'get_open_orders. Use `asset` instead.')
@api_method @api_method
def get_open_orders(self, asset=None): 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.
"""
return self._get_open_orders(asset) return self._get_open_orders(asset)
@api_method @api_method
def get_order(self, order_id): def get_order(self, order_id, exchange_name):
return self.exchange.get_order(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.
execution_price: float
The execution price per share of the order
"""
exchange = self.exchanges[exchange_name]
return exchange.get_order(order_id)
@api_method @api_method
def cancel_order(self, order_param): def cancel_order(self, order_param, exchange_name):
"""Cancel an open order.
Parameters
----------
order_param : str or Order
The order_id or order object to cancel.
"""
exchange = self.exchanges[exchange_name]
order_id = order_param order_id = order_param
if isinstance(order_param, zp.Order): if isinstance(order_param, zp.Order):
order_id = order_param.id order_id = order_param.id
self.exchange.cancel_order(order_id)
exchange.cancel_order(order_id)
+98
View File
@@ -0,0 +1,98 @@
import numpy as np
from catalyst import get_calendar
from catalyst.data.minute_bars import BcolzMinuteBarReader, \
BcolzMinuteBarWriter
class BcolzExchangeBarWriter(BcolzMinuteBarWriter):
def __init__(self, *args, **kwargs):
self._data_frequency = kwargs.pop('data_frequency', None)
kwargs.pop('minutes_per_day', None)
kwargs.pop('calendar', None)
end_session = kwargs.pop('end_session', None)
if end_session is not None:
end_session = end_session.floor('1d')
minutes_per_day = 1440 if self._data_frequency == 'minute' else 1
default_ohlc_ratio = kwargs.pop('default_ohlc_ratio', 100000000)
calendar = get_calendar('OPEN')
super(BcolzExchangeBarWriter, self) \
.__init__(*args, **dict(kwargs,
minutes_per_day=minutes_per_day,
default_ohlc_ratio=default_ohlc_ratio,
calendar=calendar,
end_session=end_session
))
class BcolzExchangeBarReader(BcolzMinuteBarReader):
def __init__(self, *args, **kwargs):
self._data_frequency = kwargs.pop('data_frequency', None)
super(BcolzExchangeBarReader, self).__init__(*args, **kwargs)
@property
def data_frequency(self):
return self._data_frequency
def load_raw_arrays(self, fields, start_dt, end_dt, sids):
"""
Parameters
----------
fields : list of str
'open', 'high', 'low', 'close', or 'volume'
start_dt: Timestamp
Beginning of the window range.
end_dt: Timestamp
End of the window range.
sids : list of int
The asset identifiers in the window.
Returns
-------
list of np.ndarray
A list with an entry per field of ndarrays with shape
(minutes in range, sids) with a dtype of float64, containing the
values for the respective field over start and end dt range.
"""
start_idx = self._find_position_of_minute(start_dt)
end_idx = self._find_position_of_minute(end_dt)
periods = self.calendar.minutes_in_range(start_dt, end_dt) \
if self.data_frequency == 'minute' \
else self.calendar.sessions_in_range(start_dt, end_dt)
num_days = len(periods)
shape = num_days, len(sids)
all_fields = fields[:]
if len(all_fields) == 1 and all_fields[0] == 'volume':
all_fields.insert(0, 'close')
mask = None
data = []
for field in all_fields:
if field != 'volume':
out = np.full(shape, np.nan)
else:
out = np.zeros(shape, dtype=np.float64)
for i, sid in enumerate(sids):
carray = self._open_minute_file(field, sid)
a = carray[start_idx:end_idx + 1]
if mask is None:
mask = a != 0
inverse_ratio = self._ohlc_ratio_inverse_for_sid(sid)
out[:len(mask), i][mask] = (
a[mask] * inverse_ratio
)
if field in fields:
data.append(out)
return data
+134
View File
@@ -0,0 +1,134 @@
from catalyst.assets._assets import TradingPair
from logbook import Logger
from catalyst.constants import LOG_LEVEL
from catalyst.finance.blotter import Blotter
from catalyst.finance.commission import CommissionModel
from catalyst.finance.slippage import SlippageModel
from catalyst.finance.transaction import create_transaction
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):
"""
Calculates a commission for a transaction based on a per percentage fee.
Parameters
----------
fee : float, optional
The percentage fee.
"""
def __init__(self,
maker_fee=DEFAULT_MAKER_FEE,
taker_fee=DEFAULT_TAKER_FEE):
self.maker_fee = maker_fee
self.taker_fee = taker_fee
def __repr__(self):
return (
'{class_name}(maker_fee={maker_fee}, '
'taker_fee={taker_fee})'.format(
class_name=self.__class__.__name__,
maker_fee=self.maker_fee,
taker_fee=self.taker_fee,
)
)
def calculate(self, order, transaction):
"""
Calculate the final fee based on the order parameters.
:param order:
:param transaction:
:return float:
The total commission.
"""
cost = abs(transaction.amount) * transaction.price
# Assuming just the taker fee for now
fee = cost * self.taker_fee
return fee
class TradingPairFixedSlippage(SlippageModel):
"""
Model slippage as a fixed spread.
Parameters
----------
spread : float, optional
spread / 2 will be added to buys and subtracted from sells.
"""
def __init__(self, spread=DEFAULT_SLIPPAGE_SPREAD):
super(TradingPairFixedSlippage, self).__init__()
self.spread = spread
def __repr__(self):
return '{class_name}(spread={spread})'.format(
class_name=self.__class__.__name__, spread=self.spread,
)
def simulate(self, data, asset, orders_for_asset):
self._volume_for_bar = 0
price = data.current(asset, 'close')
dt = data.current_dt
for order in orders_for_asset:
if order.open_amount == 0:
continue
order.check_triggers(price, dt)
if not order.triggered:
log.debug('order has not reached the trigger at current '
'price {}'.format(price))
continue
execution_price, execution_volume = self.process_order(data, order)
transaction = create_transaction(
order, dt, execution_price, execution_volume
)
self._volume_for_bar += abs(transaction.amount)
yield order, transaction
def process_order(self, data, order):
price = data.current(order.asset, 'close')
if order.amount > 0:
# Buy order
adj_price = price * (1 + self.spread)
else:
# Sell order
adj_price = price * (1 - self.spread)
log.debug('added slippage to price: {} => {}'.format(price, adj_price))
return adj_price, order.amount
class ExchangeBlotter(Blotter):
def __init__(self, *args, **kwargs):
super(ExchangeBlotter, self).__init__(*args, **kwargs)
# Using the equity models for now
# We may be able to define more sophisticated models based on the fee
# structure of each exchange.
self.slippage_models = {
TradingPair: TradingPairFixedSlippage()
}
self.commission_models = {
TradingPair: TradingPairFeeSchedule()
}
File diff suppressed because it is too large Load Diff
+406
View File
@@ -0,0 +1,406 @@
import abc
from time import sleep
import numpy as np
import pandas as pd
from catalyst.assets._assets import TradingPair
from logbook import Logger
from catalyst.constants import LOG_LEVEL, AUTO_INGEST
from catalyst.data.data_portal import DataPortal
from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.exchange_errors import (
ExchangeRequestError,
ExchangeBarDataError,
PricingDataNotLoadedError)
from catalyst.exchange.exchange_utils import get_frequency, resample_history_df
log = Logger('DataPortalExchange', level=LOG_LEVEL)
class DataPortalExchangeBase(DataPortal):
def __init__(self, *args, **kwargs):
# TODO: put somewhere accessible by each algo
self.retry_get_history_window = 5
self.retry_get_spot_value = 5
self.retry_delay = 5
super(DataPortalExchangeBase, self).__init__(*args, **kwargs)
def _get_history_window(self,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill=True,
attempt_index=0):
try:
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(exchange_assets) > 1:
df_list = []
for exchange_name in exchange_assets:
assets = exchange_assets[exchange_name]
df_exchange = self.get_exchange_history_window(
exchange_name,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill)
df_list.append(df_exchange)
# Merging the values values of each exchange
return pd.concat(df_list)
else:
exchange_name = list(exchange_assets.keys())[0]
return self.get_exchange_history_window(
exchange_name,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill)
except ExchangeRequestError as e:
log.warn(
'get history attempt {}: {}'.format(attempt_index, e)
)
if attempt_index < self.retry_get_history_window:
sleep(self.retry_delay)
return self._get_history_window(assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill,
attempt_index + 1)
else:
raise ExchangeBarDataError(
data_type='history',
attempts=attempt_index,
error=e
)
def get_history_window(self,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency=None,
ffill=True):
if field == 'price':
field = 'close'
return self._get_history_window(assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill)
@abc.abstractmethod
def get_exchange_history_window(self,
exchange_name,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill=True):
pass
def _get_spot_value(self, assets, field, dt, data_frequency,
attempt_index=0):
try:
if isinstance(assets, TradingPair):
spot_values = self.get_exchange_spot_value(
assets.exchange, [assets], field, dt, data_frequency)
if not spot_values:
return np.nan
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:
spot_values = []
for exchange_name in exchange_assets:
assets = exchange_assets[exchange_name]
exchange_spot_values = self.get_exchange_spot_value(
exchange_name,
assets,
field,
dt,
data_frequency
)
if len(assets) == 1:
spot_values.append(exchange_spot_values)
else:
spot_values += exchange_spot_values
return spot_values
except ExchangeRequestError as e:
log.warn(
'get spot value attempt {}: {}'.format(attempt_index, e)
)
if attempt_index < self.retry_get_spot_value:
sleep(self.retry_delay)
return self._get_spot_value(assets, field, dt, data_frequency,
attempt_index + 1)
else:
raise ExchangeBarDataError(
data_type='spot',
attempts=attempt_index,
error=e
)
def get_spot_value(self, assets, field, dt, data_frequency):
if field == 'price':
field = 'close'
return self._get_spot_value(assets, field, dt, data_frequency)
@abc.abstractmethod
def get_exchange_spot_value(self, exchange_name, assets, field, dt,
data_frequency):
return
def get_adjusted_value(self, asset, field, dt,
perspective_dt,
data_frequency,
spot_value=None):
# TODO: does this pertain to cryptocurrencies?
log.warn('get_adjusted_value is not implemented yet!')
return spot_value
class DataPortalExchangeLive(DataPortalExchangeBase):
def __init__(self, *args, **kwargs):
self.exchanges = kwargs.pop('exchanges', None)
super(DataPortalExchangeLive, self).__init__(*args, **kwargs)
def get_exchange_history_window(self,
exchange_name,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill=True):
"""
Fetching price history window from the exchange.
Parameters
----------
exchange_name: Exchange
assets: list[TradingPair]
end_dt: datetime
bar_count: int
frequency: str
field: str
data_frequency: str
ffill: bool
Returns
-------
DataFrame
"""
exchange = self.exchanges[exchange_name]
df = exchange.get_history_window(
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill)
return df
def get_exchange_spot_value(self, exchange_name, assets, field, dt,
data_frequency):
"""
A spot value for the exchange.
Parameters
----------
exchange_name: str
assets: list[TradingPair]
field: str
dt: datetime
data_frequency: str
Returns
-------
float
"""
exchange = self.exchanges[exchange_name]
exchange_spot_values = exchange.get_spot_value(
assets, field, dt, data_frequency)
return exchange_spot_values
class DataPortalExchangeBacktest(DataPortalExchangeBase):
def __init__(self, *args, **kwargs):
self.exchange_names = kwargs.pop('exchange_names', None)
super(DataPortalExchangeBacktest, self).__init__(*args, **kwargs)
self.exchange_bundles = dict()
self.history_loaders = dict()
self.minute_history_loaders = dict()
for name in self.exchange_names:
self.exchange_bundles[name] = ExchangeBundle(name)
def _get_first_trading_day(self, assets):
first_date = None
for asset in assets:
if first_date is None or asset.start_date > first_date:
first_date = asset.start_date
return first_date
def get_exchange_history_window(self,
exchange_name,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency,
ffill=True):
"""
Fetching price history window from the exchange bundle.
Parameters
----------
exchange: Exchange
assets: list[TradingPair]
end_dt: datetime
bar_count: int
frequency: str
field: str
data_frequency: str
ffill: bool
Returns
-------
DataFrame
"""
bundle = self.exchange_bundles[exchange_name] # type: ExchangeBundle
freq, candle_size, unit, adj_data_frequency = get_frequency(
frequency, data_frequency
)
adj_bar_count = candle_size * bar_count
trailing_bar_count = candle_size - 1
if data_frequency == 'minute' and adj_data_frequency == 'daily':
end_dt = end_dt.floor('1D')
series = bundle.get_history_window_series_and_load(
assets=assets,
end_dt=end_dt,
bar_count=adj_bar_count,
field=field,
data_frequency=adj_data_frequency,
algo_end_dt=self._last_available_session,
trailing_bar_count=trailing_bar_count
)
df = resample_history_df(pd.DataFrame(series), freq, field)
return df
def get_exchange_spot_value(self,
exchange_name,
assets,
field,
dt,
data_frequency
):
"""
A spot value for the exchange bundle. Try to ingest data if not in
the bundle.
Parameters
----------
exchange_name: str
assets: list[TradingPair]
field: str
dt: datetime
data_frequency: str
Returns
-------
float
"""
bundle = self.exchange_bundles[exchange_name]
if data_frequency == 'daily':
dt = dt.floor('1D')
else:
dt = dt.floor('1 min')
if AUTO_INGEST:
try:
return bundle.get_spot_values(
assets, field, dt, data_frequency
)
except PricingDataNotLoadedError:
log.info(
'pricing data for {symbol} not found on {dt}'
', updating the bundles.'.format(
symbol=[asset.symbol for asset in assets],
dt=dt
)
)
bundle.ingest_assets(
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)
+130 -1
View File
@@ -1,6 +1,24 @@
import sys
import traceback
from catalyst.errors import ZiplineError from catalyst.errors import ZiplineError
def silent_except_hook(exctype, excvalue, exctraceback):
if exctype in [PricingDataBeforeTradingError, PricingDataNotLoadedError,
SymbolNotFoundOnExchange, NoDataAvailableOnExchange,
ExchangeAuthEmpty]:
fn = traceback.extract_tb(exctraceback)[-1][0]
ln = traceback.extract_tb(exctraceback)[-1][1]
print("Error traceback: {1} (line {2})\n"
"{0.__name__}: {3}".format(exctype, fn, ln, excvalue))
else:
sys.__excepthook__(exctype, excvalue, exctraceback)
sys.excepthook = silent_except_hook
class ExchangeRequestError(ZiplineError): class ExchangeRequestError(ZiplineError):
msg = ( msg = (
'Request failed: {error}' 'Request failed: {error}'
@@ -34,6 +52,13 @@ class ExchangeTransactionError(ZiplineError):
).strip() ).strip()
class ExchangeNotFoundError(ZiplineError):
msg = (
'Exchange {exchange_name} not found. Please specify exchanges '
'supported by Catalyst and verify spelling for accuracy.'
).strip()
class ExchangeAuthNotFound(ZiplineError): class ExchangeAuthNotFound(ZiplineError):
msg = ( msg = (
'Please create an auth.json file containing the api token and key for ' 'Please create an auth.json file containing the api token and key for '
@@ -41,6 +66,13 @@ class ExchangeAuthNotFound(ZiplineError):
).strip() ).strip()
class ExchangeAuthEmpty(ZiplineError):
msg = (
'Please enter your API token key and secret for exchange {exchange} '
'in the following file: {filename}'
).strip()
class ExchangeSymbolsNotFound(ZiplineError): class ExchangeSymbolsNotFound(ZiplineError):
msg = ( msg = (
'Unable to download or find a local copy of symbols.json for exchange ' 'Unable to download or find a local copy of symbols.json for exchange '
@@ -54,9 +86,24 @@ class AlgoPickleNotFound(ZiplineError):
).strip() ).strip()
class InvalidHistoryFrequencyAlias(ZiplineError):
msg = (
'Invalid frequency alias {freq}. Valid suffixes are M (minute) '
'and D (day). For example, these aliases would be valid '
'1M, 5M, 1D.'
).strip()
class InvalidHistoryFrequencyError(ZiplineError): class InvalidHistoryFrequencyError(ZiplineError):
msg = ( msg = (
'History frequency {frequency} not supported by the exchange.' 'Frequency {frequency} not supported by the exchange.'
).strip()
class MismatchingFrequencyError(ZiplineError):
msg = (
'Bar aggregate frequency {frequency} not compatible with '
'data frequency {data_frequency}.'
).strip() ).strip()
@@ -87,6 +134,19 @@ class OrderNotFound(ZiplineError):
).strip() ).strip()
class OrphanOrderError(ZiplineError):
msg = (
'Order {order_id} found in exchange {exchange} but not tracked by '
'the algorithm.'
).strip()
class OrphanOrderReverseError(ZiplineError):
msg = (
'Order {order_id} tracked by algorithm, but not found in exchange {exchange}.'
).strip()
class OrderCancelError(ZiplineError): class OrderCancelError(ZiplineError):
msg = ( msg = (
'Unable to cancel order {order_id} on exchange {exchange} {error}.' 'Unable to cancel order {order_id} on exchange {exchange} {error}.'
@@ -111,3 +171,72 @@ class MismatchingBaseCurrencies(ZiplineError):
'Unable to trade with base currency {base_currency} when the ' 'Unable to trade with base currency {base_currency} when the '
'algorithm uses {algo_currency}.' 'algorithm uses {algo_currency}.'
).strip() ).strip()
class MismatchingBaseCurrenciesExchanges(ZiplineError):
msg = (
'Unable to trade with base currency {base_currency} when the '
'exchange {exchange_name} users {exchange_currency}.'
).strip()
class SymbolNotFoundOnExchange(ZiplineError):
"""
Raised when a symbol() call contains a non-existent symbol.
"""
msg = ('Symbol {symbol} not found on exchange {exchange}. '
'Choose from: {supported_symbols}').strip()
class BundleNotFoundError(ZiplineError):
msg = ('Unable to find bundle data for exchange {exchange} and '
'data frequency {data_frequency}.'
'Please ingest some price data.'
'See `catalyst ingest-exchange --help` for details.').strip()
class TempBundleNotFoundError(ZiplineError):
msg = ('Temporary bundle not found in: {path}.').strip()
class EmptyValuesInBundleError(ZiplineError):
msg = ('{name} with end minute {end_minute} has empty rows '
'in ranges: {dates}').strip()
class PricingDataBeforeTradingError(ZiplineError):
msg = ('Pricing data for trading pairs {symbols} on exchange {exchange} '
'starts on {first_trading_day}, but you are either trying to trade or '
'retrieve pricing data on {dt}. Adjust your dates accordingly.').strip()
class PricingDataNotLoadedError(ZiplineError):
msg = ('Missing data for {exchange} {symbols} in date range '
'[{start_dt} - {end_dt}]'
'\nPlease run: `catalyst ingest-exchange -x {exchange} -f '
'{data_frequency} -i {symbol_list}`. 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):
msg = ('Unable to fetch candles from the remote API: {error}.').strip()
class NoDataAvailableOnExchange(ZiplineError):
msg = (
'Requested data for trading pair {symbol} is not available on exchange {exchange} '
'in `{data_frequency}` frequency at this time. '
'Check `http://enigma.co/catalyst/status` for market coverage.').strip()
+40 -12
View File
@@ -4,9 +4,16 @@ from catalyst.finance.execution import LimitOrder, StopOrder, StopLimitOrder
class ExchangeLimitOrder(LimitOrder): class ExchangeLimitOrder(LimitOrder):
def get_limit_price(self, is_buy): def get_limit_price(self, is_buy):
""" """
We may be trading Satoshis with 8 decimals, we cannot round numbers We may be trading Satoshis with 8 decimals, we cannot round numbers.
:param is_buy:
:return: Parameters
----------
is_buy: bool
Returns
-------
float
""" """
return self.limit_price return self.limit_price
@@ -14,9 +21,16 @@ class ExchangeLimitOrder(LimitOrder):
class ExchangeStopOrder(StopOrder): class ExchangeStopOrder(StopOrder):
def get_stop_price(self, is_buy): def get_stop_price(self, is_buy):
""" """
We may be trading Satoshis with 8 decimals, we cannot round numbers We may be trading Satoshis with 8 decimals, we cannot round numbers.
:param is_buy:
:return: Parameters
----------
is_buy: bool
Returns
-------
float
""" """
return self.stop_price return self.stop_price
@@ -24,16 +38,30 @@ class ExchangeStopOrder(StopOrder):
class ExchangeStopLimitOrder(StopLimitOrder): class ExchangeStopLimitOrder(StopLimitOrder):
def get_limit_price(self, is_buy): def get_limit_price(self, is_buy):
""" """
We may be trading Satoshis with 8 decimals, we cannot round numbers We may be trading Satoshis with 8 decimals, we cannot round numbers.
:param is_buy:
:return: Parameters
----------
is_buy: bool
Returns
-------
float
""" """
return self.limit_price return self.limit_price
def get_stop_price(self, is_buy): def get_stop_price(self, is_buy):
""" """
We may be trading Satoshis with 8 decimals, we cannot round numbers We may be trading Satoshis with 8 decimals, we cannot round numbers.
:param is_buy:
:return: Parameters
----------
is_buy: bool
Returns
-------
float
""" """
return self.stop_price return self.stop_price
+57 -4
View File
@@ -1,9 +1,11 @@
import numpy as np import numpy as np
from logbook import Logger from logbook import Logger
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
log = Logger('ExchangePortfolio') log = Logger('ExchangePortfolio', level=LOG_LEVEL)
class ExchangePortfolio(Portfolio): class ExchangePortfolio(Portfolio):
@@ -28,10 +30,15 @@ class ExchangePortfolio(Portfolio):
self.positions_value = 0.0 self.positions_value = 0.0
self.open_orders = dict() self.open_orders = dict()
def calculate_pnl(self):
log.debug('calculating pnl')
def create_order(self, order): def create_order(self, order):
"""
Create an open order and store in memory.
Parameters
----------
order: Order
"""
log.debug('creating order {}'.format(order.id)) log.debug('creating order {}'.format(order.id))
self.open_orders[order.id] = order self.open_orders[order.id] = order
@@ -46,6 +53,18 @@ class ExchangePortfolio(Portfolio):
log.debug('open order added to portfolio') log.debug('open order added to portfolio')
def execute_order(self, order, transaction): def execute_order(self, order, transaction):
"""
Update the open orders and positions to apply an executed order.
Unlike with backtesting, we do not need to add slippage and fees.
The executed price includes transaction fees.
Parameters
----------
order: Order
transaction: Transaction
"""
log.debug('executing order {}'.format(order.id)) log.debug('executing order {}'.format(order.id))
del self.open_orders[order.id] del self.open_orders[order.id]
@@ -70,7 +89,41 @@ 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.
Parameters
----------
order: Order
"""
log.info('removing cancelled order {}'.format(order.id)) log.info('removing cancelled order {}'.format(order.id))
del self.open_orders[order.id] del self.open_orders[order.id]
+440 -33
View File
@@ -1,20 +1,56 @@
import hashlib
import json import json
import os import os
import pickle import pickle
import urllib 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 six.moves.urllib import request
from catalyst.exchange.exchange_errors import ExchangeAuthNotFound, \ from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
ExchangeSymbolsNotFound from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound, \
from catalyst.utils.paths import data_root, ensure_directory InvalidHistoryFrequencyError, InvalidHistoryFrequencyAlias
from catalyst.utils.paths import data_root, ensure_directory, \
last_modified_time
# TODO: move to aws
SYMBOLS_URL = 'https://raw.githubusercontent.com/enigmampc/catalyst/' \ def get_sid(symbol):
'master/catalyst/exchange/{exchange}/symbols.json' """
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):
"""
The root path of an exchange folder.
Parameters
----------
exchange_name: str
environ:
Returns
-------
str
"""
if not environ: if not environ:
environ = os.environ environ = os.environ
@@ -25,26 +61,75 @@ def get_exchange_folder(exchange_name, environ=None):
return exchange_folder return exchange_folder
def download_exchange_symbols(exchange_name, environ=None): def get_exchange_symbols_filename(exchange_name, is_local=False, environ=None):
exchange_folder = get_exchange_folder(exchange_name, environ) """
filename = os.path.join(exchange_folder, 'symbols.json') The absolute path of the exchange's symbol.json file.
Parameters
----------
exchange_name:
environ:
Returns
-------
str
"""
name = 'symbols.json' if not is_local else 'symbols_local.json'
exchange_folder = get_exchange_folder(exchange_name, environ)
return os.path.join(exchange_folder, name)
def download_exchange_symbols(exchange_name, environ=None):
"""
Downloads the exchange's symbols.json from the repository.
Parameters
----------
exchange_name: str
environ:
Returns
-------
str
"""
filename = get_exchange_symbols_filename(exchange_name)
url = SYMBOLS_URL.format(exchange=exchange_name) url = SYMBOLS_URL.format(exchange=exchange_name)
response = urllib.urlretrieve(url=url, filename=filename) response = request.urlretrieve(url=url, filename=filename)
return response return response
def get_exchange_symbols(exchange_name, environ=None): def get_exchange_symbols(exchange_name, is_local=False, environ=None):
exchange_folder = get_exchange_folder(exchange_name, environ) """
filename = os.path.join(exchange_folder, 'symbols.json') The de-serialized content of the exchange's symbols.json.
if not os.path.isfile(filename): Parameters
----------
exchange_name: str
is_local: bool
environ:
Returns
-------
Object
"""
filename = get_exchange_symbols_filename(exchange_name, is_local)
if not is_local and (not os.path.isfile(filename) or pd.Timedelta(
pd.Timestamp('now', tz='UTC') - last_modified_time(
filename)).days > 1):
download_exchange_symbols(exchange_name, environ) download_exchange_symbols(exchange_name, environ)
if os.path.isfile(filename): if os.path.isfile(filename):
with open(filename) as data_file: with open(filename) as data_file:
try:
data = json.load(data_file) data = json.load(data_file)
return data return data
except ValueError:
return dict()
else: else:
raise ExchangeSymbolsNotFound( raise ExchangeSymbolsNotFound(
exchange=exchange_name, exchange=exchange_name,
@@ -52,7 +137,63 @@ 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):
"""
A concatenated string of symbols from a list of assets.
Parameters
----------
assets: list[TradingPair]
Returns
-------
str
"""
array = [assets] if isinstance(assets, TradingPair) else assets
return ', '.join([asset.symbol for asset in array])
def get_exchange_auth(exchange_name, environ=None): def get_exchange_auth(exchange_name, environ=None):
"""
The de-serialized contend of the exchange's auth.json file.
Parameters
----------
exchange_name: str
environ:
Returns
-------
Object
"""
exchange_folder = get_exchange_folder(exchange_name, environ) exchange_folder = get_exchange_folder(exchange_name, environ)
filename = os.path.join(exchange_folder, 'auth.json') filename = os.path.join(exchange_folder, 'auth.json')
@@ -61,13 +202,45 @@ def get_exchange_auth(exchange_name, environ=None):
data = json.load(data_file) data = json.load(data_file)
return data return data
else: else:
raise ExchangeAuthNotFound( data = dict(name=exchange_name, key='', secret='')
exchange=exchange_name, with open(filename, 'w') as f:
filename=filename json.dump(data, f, sort_keys=False, indent=2,
) separators=(',', ':'))
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.
Parameters
----------
algo_name: str
environ:
Returns
-------
str
"""
if not environ: if not environ:
environ = os.environ environ = os.environ
@@ -79,6 +252,24 @@ def get_algo_folder(algo_name, environ=None):
def get_algo_object(algo_name, key, environ=None, rel_path=None): def get_algo_object(algo_name, key, environ=None, rel_path=None):
"""
The de-serialized object of the algo name and key.
Parameters
----------
algo_name: str
key: str
environ:
rel_path: str
Returns
-------
Object
"""
if algo_name is None:
return None
folder = get_algo_folder(algo_name, environ) folder = get_algo_folder(algo_name, environ)
if rel_path is not None: if rel_path is not None:
@@ -97,6 +288,18 @@ def get_algo_object(algo_name, key, environ=None, rel_path=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):
"""
Serialize and save an object by algo name and key.
Parameters
----------
algo_name: str
key: str
obj: Object
environ:
rel_path: str
"""
folder = get_algo_folder(algo_name, environ) folder = get_algo_folder(algo_name, environ)
if rel_path is not None: if rel_path is not None:
@@ -109,16 +312,22 @@ def save_algo_object(algo_name, key, obj, environ=None, rel_path=None):
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL) pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
def append_algo_object(algo_name, key, obj, environ=None):
algo_folder = get_algo_folder(algo_name, environ)
filename = os.path.join(algo_folder, key + '.p')
mode = 'a+b' if os.path.isfile(filename) else 'wb'
with open(filename, mode) 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):
"""
The de-serialized DataFrame of an algo name and key.
Parameters
----------
algo_name: str
key: str
environ:
rel_path: str
Returns
-------
DataFrame
"""
folder = get_algo_folder(algo_name, environ) folder = get_algo_folder(algo_name, environ)
if rel_path is not None: if rel_path is not None:
@@ -137,19 +346,43 @@ def get_algo_df(algo_name, key, environ=None, rel_path=None):
def save_algo_df(algo_name, key, df, environ=None, rel_path=None): def save_algo_df(algo_name, key, df, environ=None, rel_path=None):
folder = get_algo_folder(algo_name, environ) """
Serialize to csv and save a DataFrame by algo name and key.
Parameters
----------
algo_name: str
key: str
df: pd.DataFrame
environ:
rel_path: str
"""
folder = get_algo_folder(algo_name, environ)
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)
ensure_directory(folder) ensure_directory(folder)
filename = os.path.join(folder, key + '.csv') filename = os.path.join(folder, key + '.csv')
with open(filename, 'wb') as handle: with open(filename, 'wt') as handle:
df.to_csv(handle) df.to_csv(handle, encoding='UTF_8')
def get_exchange_minute_writer_root(exchange_name, environ=None): def get_exchange_minute_writer_root(exchange_name, environ=None):
"""
The minute writer folder for the exchange.
Parameters
----------
exchange_name: str
environ:
Returns
-------
BcolzExchangeBarWriter
"""
exchange_folder = get_exchange_folder(exchange_name, environ) exchange_folder = get_exchange_folder(exchange_name, environ)
minute_data_folder = os.path.join(exchange_folder, 'minute_data') minute_data_folder = os.path.join(exchange_folder, 'minute_data')
@@ -158,9 +391,183 @@ def get_exchange_minute_writer_root(exchange_name, environ=None):
return minute_data_folder return minute_data_folder
def perf_serial(obj): def get_exchange_bundles_folder(exchange_name, environ=None):
"""JSON serializer for objects not serializable by default json code""" """
The temp folder for bundle downloads by algo name.
Parameters
----------
exchange_name: str
environ:
Returns
-------
str
"""
exchange_folder = get_exchange_folder(exchange_name, environ)
temp_bundles = os.path.join(exchange_folder, 'temp_bundles')
ensure_directory(temp_bundles)
return temp_bundles
def symbols_serial(obj):
"""
JSON serializer for objects not serializable by default json code
Parameters
----------
obj: Object
Returns
-------
str
"""
if isinstance(obj, (datetime, date)):
return obj.floor('1D').strftime(DATE_FORMAT)
raise TypeError("Type %s not serializable" % type(obj))
def perf_serial(obj):
"""
JSON serializer for objects not serializable by default json code
Parameters
----------
obj: Object
Returns
-------
str
"""
if isinstance(obj, (datetime, date)): if isinstance(obj, (datetime, date)):
return obj.isoformat() return obj.isoformat()
raise TypeError("Type %s not serializable" % type(obj)) raise TypeError("Type %s not serializable" % type(obj))
def get_common_assets(exchanges):
"""
The assets available in all specified exchanges.
Parameters
----------
exchanges: list[Exchange]
Returns
-------
list[TradingPair]
"""
symbols = []
for exchange_name in exchanges:
s = [asset.symbol for asset in exchanges[exchange_name].get_assets()]
symbols.append(s)
inter_symbols = set.intersection(*map(set, symbols))
assets = []
for symbol in inter_symbols:
for exchange_name in exchanges:
asset = exchanges[exchange_name].get_asset(symbol)
assets.append(asset)
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):
"""
Resample the OHCLV DataFrame using the specified frequency.
Parameters
----------
df: DataFrame
freq: str
field: str
Returns
-------
DataFrame
"""
if field == 'open':
agg = 'first'
elif field == 'high':
agg = 'max'
elif field == 'low':
agg = 'min'
elif field == 'close':
agg = 'last'
elif field == 'volume':
agg = 'sum'
else:
raise ValueError('Invalid field.')
resampled_df = df.resample(freq).agg(agg)
return resampled_df
+43
View File
@@ -0,0 +1,43 @@
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
+56 -33
View File
@@ -1,30 +1,15 @@
#
# 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 datetime import timedelta
import matplotlib.dates as mdates
import pandas as pd import pandas as pd
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 matplotlib import pyplot as plt
from matplotlib import style
log = Logger('LiveGraphClock') from catalyst.constants import LOG_LEVEL
from catalyst.exchange.exchange_errors import \
MismatchingBaseCurrenciesExchanges
fmt = mdates.DateFormatter('%Y-%m-%d %H:%M') log = Logger('LiveGraphClock', level=LOG_LEVEL)
class LiveGraphClock(object): class LiveGraphClock(object):
@@ -35,8 +20,8 @@ class LiveGraphClock(object):
This mixes the clock with a live graph. This mixes the clock with a live graph.
Note Notes
---- -----
This seemingly awkward approach allows us to run the program using a single This seemingly awkward approach allows us to run the program using a single
thread. This is important because Matplotlib does not play nice with thread. This is important because Matplotlib does not play nice with
multi-threaded environments. Zipline probably does not either. multi-threaded environments. Zipline probably does not either.
@@ -55,11 +40,17 @@ class LiveGraphClock(object):
def __init__(self, sessions, context, time_skew=pd.Timedelta('0s')): def __init__(self, sessions, context, 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')
style.use('dark_background') style.use('dark_background')
@@ -91,13 +82,14 @@ class LiveGraphClock(object):
""" """
Trying to assign reasonable parameters to the time axis. Trying to assign reasonable parameters to the time axis.
TODO: room for improvement Parameters
----------
ax:
:param ax:
:return:
""" """
# TODO: room for improvement
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1)) ax.xaxis.set_major_locator(mdates.DayLocator(interval=1))
ax.xaxis.set_major_formatter(fmt) ax.xaxis.set_major_formatter(self.fmt)
locator = mdates.HourLocator(interval=4) locator = mdates.HourLocator(interval=4)
locator.MAXTICKS = 5000 locator.MAXTICKS = 5000
@@ -109,9 +101,21 @@ class LiveGraphClock(object):
ax.grid(True) ax.grid(True)
def set_legend(self, ax): def set_legend(self, ax):
"""
Set legend on the chart.
Parameters
----------
ax
"""
ax.legend(loc='upper left', ncol=1, fontsize=10, numpoints=1) ax.legend(loc='upper left', ncol=1, fontsize=10, numpoints=1)
def draw_pnl(self): def draw_pnl(self):
"""
Draw p&l line on the chart.
"""
ax = self.ax_pnl ax = self.ax_pnl
df = self.context.pnl_stats df = self.context.pnl_stats
@@ -132,6 +136,10 @@ class LiveGraphClock(object):
self.format_ax(ax) self.format_ax(ax)
def draw_custom_signals(self): def draw_custom_signals(self):
"""
Draw custom signals on the chart.
"""
ax = self.ax_custom_signals ax = self.ax_custom_signals
df = self.context.custom_signals_stats df = self.context.custom_signals_stats
@@ -150,21 +158,39 @@ class LiveGraphClock(object):
self.format_ax(ax) self.format_ax(ax)
def draw_exposure(self): def draw_exposure(self):
"""
Draw exposure line on the chart.
"""
ax = self.ax_exposure ax = self.ax_exposure
context = self.context context = self.context
df = context.exposure_stats 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.clear()
ax.set_title('Exposure') ax.set_title('Exposure')
ax.plot(df.index, df['base_currency'], '-', ax.plot(df.index, df['base_currency'], '-',
color='green', color='green',
linewidth=1.0, linewidth=1.0,
label='Base Currency: {}'.format( label='Base Currency: {}'.format(base_currency.upper())
context.exchange.base_currency.upper()
)
) )
positions = context.exchange.portfolio.positions
symbols = [] symbols = []
for position in positions: for position in positions:
symbols.append(position.symbol) symbols.append(position.symbol)
@@ -172,10 +198,7 @@ class LiveGraphClock(object):
ax.plot(df.index, df['long_exposure'], '-', ax.plot(df.index, df['long_exposure'], '-',
color='blue', color='blue',
linewidth=1.0, linewidth=1.0,
label='Long Exposure: {}'.format( label='Long Exposure: {}'.format(', '.join(symbols).upper()))
', '.join(symbols).upper()
)
)
self.set_legend(ax) self.set_legend(ax)
self.format_ax(ax) self.format_ax(ax)
+655
View File
@@ -0,0 +1,655 @@
import json
import json
import time
from collections import defaultdict
import numpy as np
import pandas as pd
import pytz
from catalyst.assets._assets import TradingPair
from logbook import Logger
# import six
from six import iteritems
from catalyst.constants import LOG_LEVEL
# from websocket import create_connection
from catalyst.exchange.exchange import Exchange
from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.exchange_errors import (
ExchangeRequestError,
InvalidHistoryFrequencyError,
InvalidOrderStyle, OrphanOrderReverseError)
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
ExchangeStopLimitOrder
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename, \
download_exchange_symbols, get_symbols_string
from catalyst.exchange.poloniex.poloniex_api import Poloniex_api
from catalyst.finance.order import Order, ORDER_STATUS
from catalyst.finance.transaction import Transaction
from catalyst.protocol import Account
log = Logger('Poloniex', level=LOG_LEVEL)
class Poloniex(Exchange):
def __init__(self, key, secret, base_currency, portfolio=None):
self.api = Poloniex_api(key=key, secret=secret)
self.name = 'poloniex'
self.assets = dict()
self.load_assets()
self.local_assets = dict()
self.load_assets(is_local=True)
self.base_currency = base_currency
self._portfolio = portfolio
self.minute_writer = None
self.minute_reader = None
self.transactions = defaultdict(list)
self.num_candles_limit = 2000
self.max_requests_per_minute = 60
self.request_cpt = dict()
self.bundle = ExchangeBundle(self.name)
def sanitize_curency_symbol(self, exchange_symbol):
"""
Helper method used to build the universal pair.
Include any symbol mapping here if appropriate.
:param exchange_symbol:
:return universal_symbol:
"""
return exchange_symbol.lower()
def _create_order(self, order_status):
"""
Create a Catalyst order object from the Exchange order dictionary
:param order_status:
:return: Order
"""
# if order_status['is_cancelled']:
# status = ORDER_STATUS.CANCELLED
# elif not order_status['is_live']:
# log.info('found executed order {}'.format(order_status))
# status = ORDER_STATUS.FILLED
# else:
status = ORDER_STATUS.OPEN
amount = float(order_status['amount'])
# filled = float(order_status['executed_amount'])
filled = None
if order_status['type'] == 'sell':
amount = -amount
# filled = -filled
price = float(order_status['rate'])
order_type = order_status['type']
stop_price = None
limit_price = None
# TODO: is this comprehensive enough?
# if order_type.endswith('limit'):
# limit_price = price
# elif order_type.endswith('stop'):
# stop_price = price
# executed_price = float(order_status['avg_execution_price'])
executed_price = price
# TODO: bitfinex does not specify comission. I could calculate it but not sure if it's worth it.
commission = None
# date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp']))
# date = pytz.utc.localize(date)
date = None
order = Order(
dt=date,
asset=self.assets[order_status['symbol']],
# No such field in Poloniex
amount=amount,
stop=stop_price,
limit=limit_price,
filled=filled,
id=str(order_status['orderNumber']),
commission=commission
)
order.status = status
return order, executed_price
def get_balances(self):
balances = self.api.returnbalances()
try:
log.debug('retrieving wallets balances')
except Exception as e:
log.debug(e)
raise ExchangeRequestError(error=e)
if 'error' in balances:
raise ExchangeRequestError(
error='unable to fetch balance {}'.format(balances['error'])
)
std_balances = dict()
for (key, value) in iteritems(balances):
currency = key.lower()
std_balances[currency] = float(value)
return std_balances
@property
def account(self):
account = Account()
account.settled_cash = None
account.accrued_interest = None
account.buying_power = None
account.equity_with_loan = None
account.total_positions_value = None
account.total_positions_exposure = None
account.regt_equity = None
account.regt_margin = None
account.initial_margin_requirement = None
account.maintenance_margin_requirement = None
account.available_funds = None
account.excess_liquidity = None
account.cushion = None
account.day_trades_remaining = None
account.leverage = None
account.net_leverage = None
account.net_liquidation = None
return account
@property
def time_skew(self):
# TODO: research the time skew conditions
return pd.Timedelta('0s')
def get_account(self):
# TODO: fetch account data and keep in cache
return None
def get_candles(self, freq, assets, bar_count=None,
start_dt=None, end_dt=None):
"""
Retrieve OHLVC candles from Poloniex
:param freq:
:param assets:
:param bar_count:
:return:
Available Frequencies
---------------------
'5m', '15m', '30m', '2h', '4h', '1D'
"""
if end_dt is None:
end_dt = pd.Timestamp.utcnow()
log.debug(
'retrieving {bars} {freq} candles on {exchange} from '
'{end_dt} for markets {symbols}, '.format(
bars=bar_count,
freq=freq,
exchange=self.name,
end_dt=end_dt,
symbols=get_symbols_string(assets)
)
)
if freq == '1T' and (bar_count == 1 or bar_count is None):
# TODO: use the order book instead
# We use the 5m to fetch the last bar
frequency = 300
elif freq == '5T':
frequency = 300
elif freq == '15T':
frequency = 900
elif freq == '30T':
frequency = 1800
elif freq == '120T':
frequency = 7200
elif freq == '240T':
frequency = 14400
elif freq == '1D':
frequency = 86400
else:
# Poloniex does not offer 1m data candles
# It is likely to error out there frequently
raise InvalidHistoryFrequencyError(frequency=freq)
# Making sure that assets are iterable
asset_list = [assets] if isinstance(assets, TradingPair) else assets
ohlc_map = dict()
for asset in asset_list:
delta = end_dt - pd.to_datetime('1970-1-1', utc=True)
end = int(delta.total_seconds())
if bar_count is None:
start = end - 2 * frequency
else:
start = end - bar_count * frequency
try:
response = self.api.returnchartdata(
self.get_symbol(asset), frequency, start, end
)
except Exception as e:
raise ExchangeRequestError(error=e)
if 'error' in response:
raise ExchangeRequestError(
error='Unable to retrieve candles: {}'.format(
response.content)
)
def ohlc_from_candle(candle):
last_traded = pd.Timestamp.utcfromtimestamp(candle['date'])
last_traded = last_traded.replace(tzinfo=pytz.UTC)
ohlc = dict(
open=np.float64(candle['open']),
high=np.float64(candle['high']),
low=np.float64(candle['low']),
close=np.float64(candle['close']),
volume=np.float64(candle['volume']),
price=np.float64(candle['close']),
last_traded=last_traded
)
return ohlc
if bar_count is None:
ohlc_map[asset] = ohlc_from_candle(response[0])
else:
ohlc_bars = []
for candle in response:
ohlc = ohlc_from_candle(candle)
ohlc_bars.append(ohlc)
ohlc_map[asset] = ohlc_bars
return ohlc_map[assets] \
if isinstance(assets, TradingPair) else ohlc_map
def create_order(self, asset, amount, is_buy, style):
"""
Creating order on the exchange.
:param asset:
:param amount:
:param is_buy:
:param style:
:return:
"""
exchange_symbol = self.get_symbol(asset)
if isinstance(style, ExchangeLimitOrder) or isinstance(style,
ExchangeStopLimitOrder):
if isinstance(style, ExchangeStopLimitOrder):
log.warn('{} will ignore the stop price'.format(self.name))
price = style.get_limit_price(is_buy)
try:
if (is_buy):
response = self.api.buy(exchange_symbol, amount, price)
else:
response = self.api.sell(exchange_symbol, -amount, price)
except Exception as e:
raise ExchangeRequestError(error=e)
date = pd.Timestamp.utcnow()
if ('orderNumber' in response):
order_id = str(response['orderNumber'])
order = Order(
dt=date,
asset=asset,
amount=amount,
stop=style.get_stop_price(is_buy),
limit=style.get_limit_price(is_buy),
id=order_id
)
return order
else:
log.warn(
'{} order failed: {}'.format('buy' if is_buy else 'sell',
response['error']))
return None
else:
raise InvalidOrderStyle(exchange=self.name,
style=style.__class__.__name__)
def get_open_orders(self, asset='all'):
"""Retrieve all of the current open orders.
Parameters
----------
asset : Asset
If passed and not 'all', return only the open orders for the given
asset instead of all open orders.
Returns
-------
open_orders : dict[list[Order]] or list[Order]
If 'all' is passed this will return a dict mapping Assets
to a list containing all the open orders for the asset.
If an asset is passed then this will return a list of the open
orders for this asset.
"""
return self.portfolio.open_orders
"""
TODO: Why going to the exchange if we already have this info locally?
And why creating all these Orders if we later discard them?
"""
try:
if (asset == 'all'):
response = self.api.returnopenorders('all')
else:
response = self.api.returnopenorders(self.get_symbol(asset))
except Exception as e:
raise ExchangeRequestError(error=e)
if 'error' in response:
raise ExchangeRequestError(
error='Unable to retrieve open orders: {}'.format(
order_statuses['message'])
)
print(self.portfolio.open_orders)
# TODO: Need to handle openOrders for 'all'
orders = list()
for order_status in response:
order, executed_price = self._create_order(
order_status) # will Throw error b/c Polo doesn't track order['symbol']
if asset is None or asset == order.sid:
orders.append(order)
return orders
def get_order(self, order_id):
"""Lookup an order based on the order id returned from one of the
order functions.
Parameters
----------
order_id : str
The unique identifier for the order.
Returns
-------
order : Order
The order object.
"""
try:
order = self._portfolio.open_orders[order_id]
except Exception as e:
raise OrphanOrderError(order_id=order_id, exchange=self.name)
return order
# TODO: Need to decide whether we fetch orders locally or from exchnage
# The code below is ignored
try:
response = self.api.returnopenorders(self.get_symbol(order.sid))
except Exception as e:
raise ExchangeRequestError(error=e)
for o in response:
if (int(o['orderNumber']) == int(order_id)):
return order
return None
def cancel_order(self, order_param):
"""Cancel an open order.
Parameters
----------
order_param : str or Order
The order_id or order object to cancel.
"""
if (isinstance(order_param, Order)):
order = order_param
else:
order = self._portfolio.open_orders[order_param]
try:
response = self.api.cancelorder(order.id)
except Exception as e:
raise ExchangeRequestError(error=e)
if 'error' in response:
log.info(
'Unable to cancel order {order_id} on exchange {exchange} {error}.'.format(
order_id=order.id,
exchange=self.name,
error=response['error']
))
# raise OrderCancelError(
# order_id=order.id,
# exchange=self.name,
# error=response['error']
# )
self.portfolio.remove_order(order)
def tickers(self, assets):
"""
Fetch ticket data for assets
https://docs.bitfinex.com/v2/reference#rest-public-tickers
:param assets:
:return:
"""
symbols = self.get_symbols(assets)
log.debug('fetching tickers {}'.format(symbols))
try:
response = self.api.returnticker()
except Exception as e:
raise ExchangeRequestError(error=e)
if 'error' in response:
raise ExchangeRequestError(
error='Unable to retrieve tickers: {}'.format(
response['error'])
)
ticks = dict()
for index, symbol in enumerate(symbols):
ticks[assets[index]] = dict(
timestamp=pd.Timestamp.utcnow(),
bid=float(response[symbol]['highestBid']),
ask=float(response[symbol]['lowestAsk']),
last_price=float(response[symbol]['last']),
low=float(response[symbol]['lowestAsk']),
# TODO: Polo does not provide low
high=float(response[symbol]['highestBid']),
# TODO: Polo does not provide high
volume=float(response[symbol]['baseVolume']),
)
log.debug('got tickers {}'.format(ticks))
return ticks
def generate_symbols_json(self, filename=None, source_dates=False):
symbol_map = {}
if not source_dates:
fn, r = download_exchange_symbols(self.name)
with open(fn) as data_file:
cached_symbols = json.load(data_file)
response = self.api.returnticker()
for exchange_symbol in response:
base, market = self.sanitize_curency_symbol(exchange_symbol).split(
'_')
symbol = '{market}_{base}'.format(market=market, base=base)
if (source_dates):
start_date = self.get_symbol_start_date(exchange_symbol)
else:
try:
start_date = cached_symbols[exchange_symbol]['start_date']
except KeyError as e:
start_date = time.strftime('%Y-%m-%d')
try:
end_daily = cached_symbols[exchange_symbol]['end_daily']
except KeyError as e:
end_daily = 'N/A'
try:
end_minute = cached_symbols[exchange_symbol]['end_minute']
except KeyError as e:
end_minute = 'N/A'
symbol_map[exchange_symbol] = dict(
symbol=symbol,
start_date=start_date,
end_daily=end_daily,
end_minute=end_minute,
)
if (filename is None):
filename = get_exchange_symbols_filename(self.name)
with open(filename, 'w') as f:
json.dump(symbol_map, f, sort_keys=True, indent=2,
separators=(',', ':'))
def get_symbol_start_date(self, symbol):
try:
r = self.api.returnchartdata(symbol, 86400, pd.to_datetime(
'2010-1-1').value // 10 ** 9)
except Exception as e:
raise ExchangeRequestError(error=e)
return time.strftime('%Y-%m-%d', time.gmtime(int(r[0]['date'])))
def check_open_orders(self):
"""
Need to override this function for Poloniex:
Loop through the list of open orders in the Portfolio object.
Check if any transactions have been executed:
If so, create a transaction and apply to the Portfolio.
Check if the order is still open:
If not, remove it from open orders
:return:
transactions: Transaction[]
"""
transactions = list()
if self.portfolio.open_orders:
for order_id in list(self.portfolio.open_orders):
order = self._portfolio.open_orders[order_id]
log.debug('found open order: {}'.format(order_id))
try:
order_open = self.get_order(order_id)
except Exception as e:
raise ExchangeRequestError(error=e)
if (order_open):
delta = pd.Timestamp.utcnow() - order.dt
log.info(
'order {order_id} still open after {delta}'.format(
order_id=order_id,
delta=delta)
)
try:
response = self.api.returnordertrades(order_id)
except Exception as e:
raise ExchangeRequestError(error=e)
if ('error' in response):
if (not order_open):
raise OrphanOrderReverseError(order_id=order_id,
exchange=self.name)
else:
for tx in response:
"""
We maintain a list of dictionaries of transactions that correspond to
partially filled orders, indexed by order_id. Every time we query
executed transactions from the exchange, we check if we had that
transaction for that order already. If not, we process it.
When an order if fully filled, we flush the dict of transactions
associated with that order.
"""
if (not filter(
lambda item: item['order_id'] == tx['tradeID'],
self.transactions[order_id])):
log.debug(
'Got new transaction for order {}: amount {}, price {}'.format(
order_id, tx['amount'], tx['rate']))
tx['amount'] = float(tx['amount'])
if (tx['type'] == 'sell'):
tx['amount'] = -tx['amount']
transaction = Transaction(
asset=order.asset,
amount=tx['amount'],
dt=pd.to_datetime(tx['date'], utc=True),
price=float(tx['rate']),
order_id=tx['tradeID'],
# it's a misnomer, but keeping it for compatibility
commission=float(tx['fee'])
)
self.transactions[order_id].append(transaction)
self.portfolio.execute_transaction(transaction)
transactions.append(transaction)
if (not order_open):
"""
Since transactions have been executed individually
the only thing left to do is remove them from list of open_orders
"""
del self.portfolio.open_orders[order_id]
del self.transactions[order_id]
return transactions
def get_orderbook(self, asset, order_type='all'):
exchange_symbol = asset.exchange_symbol
data = self.api.returnOrderBook(market=exchange_symbol)
result = dict()
for order_type in data:
# TODO: filter by type
if order_type != 'asks' and order_type != 'bids':
continue
result[order_type] = []
for entry in data[order_type]:
if len(entry) == 2:
result[order_type].append(
dict(
rate=float(entry[0]),
quantity=float(entry[1])
)
)
return result
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env python
import json
import time
import hmac
import hashlib
import ssl
from six.moves import urllib
# Workaround for backwards compatibility
# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7
urlopen = urllib.request.urlopen
class Poloniex_api(object):
def __init__(self, key, secret):
self.key = key
self.secret = secret
self.max_requests_per_second = 6
self.request_cpt = dict()
self.public = ['returnTicker', 'return24Volume', 'returnOrderBook',
'returnTradeHistory', 'returnChartData',
'returnCurrencies', 'returnLoanOrders']
self.trading = ['returnBalances', 'returnCompleteBalances',
'returnDepositAddresses',
'generateNewAddress', 'returnDepositsWithdrawals',
'returnOpenOrders',
'returnTradeHistory', 'returnOrderTrades',
'buy', 'sell', 'cancelOrder', 'moveOrder',
'withdraw', 'returnFeeInfo',
'returnAvailableAccountBalances',
'returnTradableBalances', 'transferBalance',
'returnMarginAccountSummary', 'marginBuy',
'marginSell',
'getMarginPosition', 'closeMarginPosition',
'createLoanOffer',
'cancelLoanOffer', 'returnOpenLoanOffers',
'returnActiveLoans',
'returnLendingHistory', 'toggleAutoRenew']
def ask_request(self):
"""
Asks permission to issue a request to the exchange.
The primary purpose is to avoid hitting rate limits.
The application will pause if the maximum requests per minute
permitted by the exchange is exceeded.
:return boolean:
"""
now = time.time()
if not self.request_cpt:
self.request_cpt = dict()
self.request_cpt[now] = 0
return True
cpt_date = list(self.request_cpt.keys())[0]
cpt = self.request_cpt[cpt_date]
if now > cpt_date + 1:
self.request_cpt = dict()
self.request_cpt[now] = 0
return True
if cpt >= self.max_requests_per_second:
time.sleep(1)
now = time.time()
self.request_cpt = dict()
self.request_cpt[now] = 0
return True
else:
self.request_cpt[cpt_date] += 1
def query(self, method, req={}):
if method in self.public:
url = 'https://poloniex.com/public?command=' + method + '&' + \
urllib.parse.urlencode(req)
headers = {}
post_data = None
elif method in self.trading:
url = 'https://poloniex.com/tradingApi'
req['command'] = method
req['nonce'] = int(time.time() * 1000)
post_data = urllib.parse.urlencode(req)
signature = hmac.new(self.secret.encode('utf-8'),
post_data.encode('utf-8'),
hashlib.sha512).hexdigest()
headers = {'Sign': signature, 'Key': self.key}
post_data = post_data.encode('utf-8')
else:
raise ValueError(
'Method "' + method + '" not found in neither the Public API '
'or Trading API endpoints'
)
self.ask_request()
req = urllib.request.Request(
url,
data=post_data,
headers=headers,
)
return json.loads(
urlopen(req, context=ssl._create_unverified_context()).read())
def returnticker(self):
return self.query('returnTicker', {})
def return24volume(self):
return self.query('return24Volume', {})
def returnOrderBook(self, market='all'):
return self.query('returnOrderBook', {'currencyPair': market})
def returntradehistory(self, market, start=None, end=None):
if (start is not None and end is not None):
return self.query('returntradehistory',
{'currencyPair': market, 'start': start,
'end': end})
else:
return self.query('returntradehistory', {'currencyPair': market})
def returnchartdata(self, market, period, start, end=9999999999):
return self.query('returnChartData',
{'currencyPair': market, 'period': period,
'start': start, 'end': end})
def returncurrencies(self):
return self.query('returnCurrencies', {})
def returnloadorders(self, market):
return self.query('returnLoanOrders', {'currency': market})
def returnbalances(self):
return self.query('returnBalances')
def returncompletebalances(self, account):
if (account):
return self.query('returnCompleteBalances', {'account': account})
else:
return self.query('returnCompleteBalances')
def returndepositaddresses(self):
return self.query('returnDepositAddresses')
def generatenewaddress(self, currency):
return self.query('generateNewAddress', {'currency': currency})
def returnDepositsWithdrawals(self, start, end):
return self.query('returnDepositsWithdrawals',
{'start': start, 'end': end})
def returnopenorders(self, market):
return self.query('returnOpenOrders', {'currencyPair': market})
def returntradehistory(self, market):
# TODO: optional start and/or end and limit
return self.query('returnTradeHistory', {'currencyPair': market})
def returnordertrades(self, ordernumber):
return self.query('returnOrderTrades', {'orderNumber': ordernumber})
def buy(self, market, amount, rate, fillorkill=0, immediateorcancel=0,
postonly=0):
if (fillorkill):
return self.query('buy', {'currencyPair': market, 'rate': rate,
'amount': amount,
'fillOrKill': fillorkill, })
elif (immediateorcancel):
return self.query('buy', {'currencyPair': market, 'rate': rate,
'amount': amount,
'immediateOrCancel': immediateorcancel, })
elif (postonly):
return self.query('buy', {'currencyPair': market, 'rate': rate,
'amount': amount,
'postOnly': postonly, })
else:
return self.query('buy', {'currencyPair': market, 'rate': rate,
'amount': amount, })
def sell(self, market, amount, rate, fillorkill=0, immediateorcancel=0,
postonly=0):
if (fillorkill):
return self.query('sell', {'currencyPair': market, 'rate': rate,
'amount': amount,
'fillOrKill': fillorkill, })
elif (immediateorcancel):
return self.query('sell', {'currencyPair': market, 'rate': rate,
'amount': amount,
'immediateOrCancel': immediateorcancel, })
elif (postonly):
return self.query('sell', {'currencyPair': market, 'rate': rate,
'amount': amount,
'postOnly': postonly, })
else:
return self.query('sell', {'currencyPair': market, 'rate': rate,
'amount': amount, })
def cancelorder(self, ordernumber):
return self.query('cancelOrder', {'orderNumber': ordernumber})
def withdraw(self, currency, quantity, address):
return self.query('withdraw',
{'currency': currency, 'amount': quantity,
'address': address})
def returnfeeinfo(self):
return self.query('returnFeeInfo')
+4 -4
View File
@@ -16,13 +16,13 @@ from time import sleep
import pandas as pd import pandas as pd
from catalyst.gens.sim_engine import ( from catalyst.gens.sim_engine import (
BAR, BAR,
SESSION_START, SESSION_START
MINUTE_END,
SESSION_END
) )
from logbook import Logger from logbook import Logger
log = Logger('ExchangeClock') from catalyst.constants import LOG_LEVEL
log = Logger('ExchangeClock', level=LOG_LEVEL)
class SimpleClock(object): class SimpleClock(object):
+178 -4
View File
@@ -1,14 +1,138 @@
import numbers
import numpy as np
import pandas as pd import pandas as pd
def get_pretty_stats(stats_df, num_rows=10): def trend_direction(series):
if series[-1] is np.nan or series[-1] is np.nan:
return None
if series[-1] > series[-2]:
return 'up'
else:
return 'down'
def crossover(source, target):
"""
The `x`-series is defined as having crossed over `y`-series if the value
of `x` is greater than the value of `y` and the value of `x` was less than
the value of `y` on the bar immediately preceding the current bar.
Parameters
----------
source: Series
target: Series
Returns
-------
bool
"""
if isinstance(target, numbers.Number):
if source[-1] is np.nan or source[-2] is np.nan \
or target is np.nan:
return False
if source[-1] >= target > source[-2]:
return True
else:
return False
else:
if source[-1] is np.nan or source[-2] is np.nan \
or target[-1] is np.nan or target[-2] is np.nan:
return False
if source[-1] > target[-1] and source[-2] < target[-2]:
return True
else:
return False
def crossunder(source, target):
"""
The `x`-series is defined as having crossed under `y`-series if the value
of `x` is less than the value of `y` and the value of `x` was greater than
the value of `y` on the bar immediately preceding the current bar.
Parameters
----------
source: Series
target: Series
Returns
-------
bool
"""
if isinstance(target, numbers.Number):
if source[-1] is np.nan or source[-2] is np.nan \
or target is np.nan:
return False
if source[-1] < target <= source[-2]:
return True
else:
return False
else:
if source[-1] is np.nan or source[-2] is np.nan \
or target[-1] is np.nan or target[-2] is np.nan:
return False
if source[-1] < target[-1] and source[-2] >= target[-2]:
return True
else:
return False
def vwap(df):
"""
Volume-weighted average price (VWAP) is a ratio generally used by
institutional investors and mutual funds to make buys and sells so as not
to disturb the market prices with large orders. It is the average share
price of a stock weighted against its trading volume within a particular
time frame, generally one day.
Read more: Volume Weighted Average Price - VWAP
https://www.investopedia.com/terms/v/vwap.asp#ixzz4xt922daE
Parameters
----------
df: pd.DataFrame
Returns
-------
"""
if 'close' not in df.columns or 'volume' not in df.columns:
raise ValueError('price data must include `volume` and `close`')
vol_sum = np.nansum(df['volume'].values)
try:
ret = np.nansum(df['close'].values * df['volume'].values) / vol_sum
except ZeroDivisionError:
ret = np.nan
return ret
def get_pretty_stats(stats_df, recorded_cols=None, num_rows=10):
""" """
Format and print the last few rows of a statistics DataFrame. Format and print the last few rows of a statistics DataFrame.
See the pyfolio project for the data structure. See the pyfolio project for the data structure.
:param stats_df: Parameters
:param num_rows: ----------
:return: stats_df: DataFrame
num_rows: int
Returns
-------
str
""" """
stats_df.set_index('period_close', drop=True, inplace=True) stats_df.set_index('period_close', drop=True, inplace=True)
stats_df.dropna(axis=1, how='all', inplace=True) stats_df.dropna(axis=1, how='all', inplace=True)
@@ -22,6 +146,10 @@ def get_pretty_stats(stats_df, num_rows=10):
'pnl', 'long_exposure', 'short_exposure', 'orders', 'pnl', 'long_exposure', 'short_exposure', 'orders',
'transactions', 'positions'] 'transactions', 'positions']
if recorded_cols is not None:
for column in recorded_cols:
columns.append(column)
def format_positions(positions): def format_positions(positions):
parts = [] parts = []
for position in positions: for position in positions:
@@ -45,3 +173,49 @@ def get_pretty_stats(stats_df, num_rows=10):
columns=columns, columns=columns,
formatters=formatters formatters=formatters
) )
def df_to_string(df):
"""
Create a formatted str representation of the DataFrame.
Parameters
----------
df: DataFrame
Returns
-------
str
"""
pd.set_option('display.expand_frame_repr', False)
pd.set_option('precision', 8)
pd.set_option('display.width', 1000)
pd.set_option('display.max_colwidth', 1000)
return df.to_string()
def extract_transactions(perf):
"""
Compute indexes for buy and sell transactions
Parameters
----------
perf: DataFrame
The algo performance DataFrame.
Returns
-------
DataFrame
A DataFrame of transactions.
"""
trans_list = perf.transactions.values
all_trans = [t for sublist in trans_list for t in sublist]
all_trans.sort(key=lambda t: t['dt'])
transactions = pd.DataFrame(all_trans)
if not transactions.empty:
transactions.set_index('dt', inplace=True, drop=True)
return transactions
+142
View File
@@ -0,0 +1,142 @@
import os
import tempfile
import pandas as pd
import six
from catalyst.assets._assets import TradingPair, get_calendar
from logbook import Logger
from pandas.util.testing import assert_frame_equal
from catalyst.constants import LOG_LEVEL
from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
from catalyst.exchange.exchange_data_portal import DataPortalExchangeBacktest
from catalyst.exchange.factory import get_exchanges
from catalyst.utils.paths import ensure_directory
log = Logger('Validator', level=LOG_LEVEL)
def output_df(df, assets, name=None):
"""
Outputs a price DataFrame to a temp folder.
Parameters
----------
df: pd.DataFrame
assets
name
Returns
-------
"""
if isinstance(assets, TradingPair):
exchange_folder = assets.exchange
asset_folder = assets.symbol
else:
exchange_folder = ','.join([asset.exchange for asset in assets])
asset_folder = ','.join([asset.symbol for asset in assets])
folder = os.path.join(
tempfile.gettempdir(), 'catalyst', exchange_folder, asset_folder
)
ensure_directory(folder)
if name is None:
name = 'output'
path = os.path.join(folder, '{}.csv'.format(name))
df.to_csv(path)
return path
class Validator(object):
def __init__(self, data_portal):
self.data_portal = data_portal
def compare_bundle_with_exchange(self, exchange, assets, end_dt, bar_count,
sample_minutes):
"""
Creates DataFrames from the bundle and exchange for the specified
data set.
Parameters
----------
exchange: Exchange
assets
end_dt
bar_count
sample_minutes
Returns
-------
"""
freq = '{}T'.format(sample_minutes)
log.info('creating data sample from bundle')
df1 = self.data_portal.get_history_window(
assets=assets,
end_dt=end_dt,
bar_count=bar_count,
frequency=freq,
field='close',
data_frequency='minute'
)
path = output_df(df1, assets, '{}_resampled'.format(freq))
log.info('saved resampled bundle candles: {}\n{}'.format(
path, df1.tail(10))
)
log.info('creating data sample from exchange api')
candles = exchange.get_candles(
end_dt=end_dt,
freq='{}T'.format(sample_minutes),
assets=assets,
bar_count=bar_count
)
series = dict()
for asset in assets:
series[asset] = pd.Series(
data=[candle['close'] for candle in candles[asset]],
index=[candle['last_traded'] for candle in candles[asset]]
)
df2 = pd.DataFrame(series)
path = output_df(df2, assets, '{}_api'.format(freq))
log.info('saved exchange api candles: {}\n{}'.format(
path, df2.tail(10))
)
try:
assert_frame_equal(df1, df2)
return True
except:
log.warn('differences found in dataframes')
return False
if __name__ == '__main__':
exchanges = get_exchanges(['poloniex'])
exchange = six.next(six.itervalues(exchanges))
assets = exchange.get_assets(symbols=['eth_btc'])
open_calendar = get_calendar('OPEN')
asset_finder = AssetFinderExchange()
data_portal = DataPortalExchangeBacktest(
exchanges=exchanges,
asset_finder=asset_finder,
trading_calendar=open_calendar,
first_trading_day=None # will set dynamically based on assets
)
validator = Validator(data_portal=data_portal)
validator.compare_bundle_with_exchange(
exchange=exchange,
assets=assets,
end_dt=pd.to_datetime('2017-11-10 1:00', utc=True),
bar_count=200,
sample_minutes=30
)
+3 -1
View File
@@ -34,7 +34,9 @@ from catalyst.finance.commission import (
from catalyst.finance.cancel_policy import NeverCancel from catalyst.finance.cancel_policy import NeverCancel
from catalyst.utils.input_validation import expect_types from catalyst.utils.input_validation import expect_types
log = Logger('Blotter') from catalyst.constants import LOG_LEVEL
log = Logger('Blotter', level=LOG_LEVEL)
warning_logger = Logger('AlgoWarning') warning_logger = Logger('AlgoWarning')
+3 -1
View File
@@ -24,7 +24,9 @@ from catalyst.errors import (
TradingControlViolation, TradingControlViolation,
) )
log = logbook.Logger('TradingControl') from catalyst.constants import LOG_LEVEL
log = logbook.Logger('TradingControl', level=LOG_LEVEL)
class TradingControl(with_metaclass(abc.ABCMeta)): class TradingControl(with_metaclass(abc.ABCMeta)):
+14 -22
View File
@@ -77,6 +77,7 @@ class LimitOrder(ExecutionStyle):
Execution style representing an order to be executed at a price equal to or Execution style representing an order to be executed at a price equal to or
better than a specified limit price. better than a specified limit price.
""" """
def __init__(self, limit_price, exchange=None): def __init__(self, limit_price, exchange=None):
""" """
Store the given price. Store the given price.
@@ -99,6 +100,7 @@ class StopOrder(ExecutionStyle):
Execution style representing an order to be placed once the market price Execution style representing an order to be placed once the market price
reaches a specified stop price. reaches a specified stop price.
""" """
def __init__(self, stop_price, exchange=None): def __init__(self, stop_price, exchange=None):
""" """
Store the given price. Store the given price.
@@ -121,6 +123,7 @@ class StopLimitOrder(ExecutionStyle):
Execution style representing a limit order to be placed with a specified Execution style representing a limit order to be placed with a specified
limit price once the market reaches a specified stop price. limit price once the market reaches a specified stop price.
""" """
def __init__(self, limit_price, stop_price, exchange=None): def __init__(self, limit_price, stop_price, exchange=None):
""" """
Store the given prices Store the given prices
@@ -144,31 +147,20 @@ class StopLimitOrder(ExecutionStyle):
def asymmetric_round_price_to_penny(price, prefer_round_down, def asymmetric_round_price_to_penny(price, prefer_round_down,
diff=(0.0095 - .005)): diff=(0.0095 - .005)):
""" """
Asymmetric rounding function for adjusting prices to two places in a way Modified the original function because we do not want to round
that "improves" the price. For limit prices, this means preferring to prices on crypto exchange.
round down on buys and preferring to round up on sells. For stop prices,
it means the reverse.
If prefer_round_down == True: Parameters
When .05 below to .95 above a penny, use that penny. ----------
If prefer_round_down == False: price: float
When .95 below to .05 above a penny, use that penny.
Returns
-------
float
In math-speak:
If prefer_round_down: [<X-1>.0095, X.0195) -> round to X.01.
If not prefer_round_down: (<X-1>.0005, X.0105] -> round to X.01.
""" """
# Subtracting an epsilon from diff to enforce the open-ness of the upper # TODO: consider overriding outside of the original function
# bound on buys and the lower bound on sells. Using the actual system return price
# epsilon doesn't quite get there, so use a slightly less epsilon-ey value.
epsilon = float_info.epsilon * 10
diff = diff - epsilon
# relies on rounding half away from zero, unlike numpy's bankers' rounding
rounded = round(price - (diff if prefer_round_down else -diff), 2)
if zp_math.tolerant_equals(rounded, 0.0):
return 0.0
return rounded
def check_stoplimit_prices(price, label): def check_stoplimit_prices(price, label):
+4 -1
View File
@@ -88,7 +88,10 @@ from six import itervalues, iteritems
import catalyst.protocol as zp import catalyst.protocol as zp
log = logbook.Logger('Performance') from catalyst.constants import LOG_LEVEL
log = logbook.Logger('Performance', level=LOG_LEVEL)
TRADE_TYPE = zp.DATASOURCE_TYPE.TRADE TRADE_TYPE = zp.DATASOURCE_TYPE.TRADE
+3 -1
View File
@@ -40,7 +40,9 @@ import logbook
from catalyst.assets import Future, Asset from catalyst.assets import Future, Asset
from catalyst.utils.input_validation import expect_types from catalyst.utils.input_validation import expect_types
log = logbook.Logger('Performance') from catalyst.constants import LOG_LEVEL
log = logbook.Logger('Performance', level=LOG_LEVEL)
class Position(object): class Position(object):
@@ -32,7 +32,9 @@ from catalyst.assets import (
) )
from . position import positiondict from . position import positiondict
log = logbook.Logger('Performance') from catalyst.constants import LOG_LEVEL
log = logbook.Logger('Performance', level=LOG_LEVEL)
PositionStats = namedtuple('PositionStats', PositionStats = namedtuple('PositionStats',
+3 -17
View File
@@ -70,7 +70,9 @@ import catalyst.finance.risk as risk
from . position_tracker import PositionTracker from . position_tracker import PositionTracker
log = logbook.Logger('Performance') from catalyst.constants import LOG_LEVEL
log = logbook.Logger('Performance', level=LOG_LEVEL)
class PerformanceTracker(object): class PerformanceTracker(object):
@@ -111,27 +113,11 @@ class PerformanceTracker(object):
self.treasury_curves, self.treasury_curves,
self.trading_calendar self.trading_calendar
) )
elif self.emission_rate == '5-minute':
self.all_benchmark_returns = pd.Series(
index=pd.date_range(
self.sim_params.first_open,
self.sim_params.last_close,
freq='5min'
),
)
self.cumulative_risk_metrics = \
risk.RiskMetricsCumulative(
self.sim_params,
self.treasury_curves,
self.trading_calendar,
create_first_day_stats=True,
)
elif self.emission_rate == 'minute': elif self.emission_rate == 'minute':
self.all_benchmark_returns = pd.Series(index=pd.date_range( self.all_benchmark_returns = pd.Series(index=pd.date_range(
self.sim_params.first_open, self.sim_params.last_close, self.sim_params.first_open, self.sim_params.last_close,
freq='Min') freq='Min')
) )
self.cumulative_risk_metrics = \ self.cumulative_risk_metrics = \
risk.RiskMetricsCumulative( risk.RiskMetricsCumulative(
self.sim_params, self.sim_params,
+18 -3
View File
@@ -37,9 +37,10 @@ from empyrical import (
sharpe_ratio, sharpe_ratio,
sortino_ratio, sortino_ratio,
) )
import warnings
from catalyst.constants import LOG_LEVEL
log = logbook.Logger('Risk Cumulative') 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)
@@ -143,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
@@ -189,9 +192,12 @@ class RiskMetricsCumulative(object):
if len(self.benchmark_returns) == 1: if len(self.benchmark_returns) == 1:
self.benchmark_returns = np.append(0.0, self.benchmark_returns) self.benchmark_returns = np.append(0.0, self.benchmark_returns)
try:
self.benchmark_cumulative_returns[dt_loc] = cum_returns( self.benchmark_cumulative_returns[dt_loc] = cum_returns(
self.benchmark_returns self.benchmark_returns
)[-1] )[-1]
except Exception:
self.benchmark_cumulative_returns[dt_loc] = 0
benchmark_cumulative_returns_to_date = \ benchmark_cumulative_returns_to_date = \
self.benchmark_cumulative_returns[:dt_loc + 1] self.benchmark_cumulative_returns[:dt_loc + 1]
@@ -266,10 +272,17 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
self.downside_risk[dt_loc] = downside_risk( self.downside_risk[dt_loc] = downside_risk(
self.algorithm_returns self.algorithm_returns
) )
try:
risk = self.downside_risk[dt_loc]
self.sortino[dt_loc] = sortino_ratio( self.sortino[dt_loc] = sortino_ratio(
self.algorithm_returns, self.algorithm_returns,
_downside_risk=self.downside_risk[dt_loc] _downside_risk=risk
) )
except Exception:
# TODO: what causes it to error out?
self.sortino[dt_loc] = 0
self.information[dt_loc] = information_ratio( self.information[dt_loc] = information_ratio(
self.algorithm_returns, self.algorithm_returns,
self.benchmark_returns, self.benchmark_returns,
@@ -281,6 +294,8 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
self.max_leverage = self.calculate_max_leverage() self.max_leverage = self.calculate_max_leverage()
self.max_leverages[dt_loc] = self.max_leverage self.max_leverages[dt_loc] = self.max_leverage
warnings.resetwarnings()
def to_dict(self): def to_dict(self):
""" """
Creates a dictionary representing the state of the risk report. Creates a dictionary representing the state of the risk report.
+20 -2
View File
@@ -14,6 +14,7 @@
# limitations under the License. # limitations under the License.
import functools import functools
import warnings
import logbook import logbook
@@ -36,7 +37,9 @@ from empyrical import (
sortino_ratio sortino_ratio
) )
log = logbook.Logger('Risk Period') from catalyst.constants import LOG_LEVEL
log = logbook.Logger('Risk Period', level=LOG_LEVEL)
choose_treasury = functools.partial(risk.choose_treasury, choose_treasury = functools.partial(risk.choose_treasury,
risk.select_treasury_duration) risk.select_treasury_duration)
@@ -76,8 +79,14 @@ class RiskMetricsPeriod(object):
self.calculate_metrics() self.calculate_metrics()
def calculate_metrics(self): def calculate_metrics(self):
warnings.filterwarnings('error')
try:
self.benchmark_period_returns = \ self.benchmark_period_returns = \
cum_returns(self.benchmark_returns).iloc[-1] 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]
@@ -126,10 +135,17 @@ class RiskMetricsPeriod(object):
self.downside_risk = downside_risk( self.downside_risk = downside_risk(
self.algorithm_returns.values self.algorithm_returns.values
) )
try:
risk = self.downside_risk
self.sortino = sortino_ratio( self.sortino = sortino_ratio(
self.algorithm_returns.values, self.algorithm_returns.values,
_downside_risk=self.downside_risk, _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,
@@ -143,6 +159,8 @@ class RiskMetricsPeriod(object):
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.
+3 -1
View File
@@ -63,7 +63,9 @@ from dateutil.relativedelta import relativedelta
from . period import RiskMetricsPeriod from . period import RiskMetricsPeriod
log = logbook.Logger('Risk Report') from catalyst.constants import LOG_LEVEL
log = logbook.Logger('Risk Report', level=LOG_LEVEL)
class RiskReport(object): class RiskReport(object):
+3 -1
View File
@@ -61,7 +61,9 @@ Risk Report
import logbook import logbook
import numpy as np import numpy as np
log = logbook.Logger('Risk') from catalyst.constants import LOG_LEVEL
log = logbook.Logger('Risk', level=LOG_LEVEL)
TREASURY_DURATIONS = [ TREASURY_DURATIONS = [
+3 -1
View File
@@ -26,7 +26,9 @@ from catalyst.data.loader import load_market_data
from catalyst.utils.calendars import get_calendar from catalyst.utils.calendars import get_calendar
from catalyst.utils.memoize import remember_last from catalyst.utils.memoize import remember_last
log = logbook.Logger('Trading') from catalyst.constants import LOG_LEVEL
log = logbook.Logger('Trading', level=LOG_LEVEL)
DEFAULT_CAPITAL_BASE = 1e5 DEFAULT_CAPITAL_BASE = 1e5
-23
View File
@@ -20,9 +20,7 @@ cimport cython
from cpython cimport bool from cpython cimport bool
cdef np.int64_t _nanos_in_minute = 60000000000 cdef np.int64_t _nanos_in_minute = 60000000000
cdef np.int64_t _nanos_in_five_minutes = 5 * _nanos_in_minute
NANOS_IN_MINUTE = _nanos_in_minute NANOS_IN_MINUTE = _nanos_in_minute
NANOS_IN_FIVE_MINUTES = _nanos_in_five_minutes
cpdef enum: cpdef enum:
BAR = 0 BAR = 0
@@ -117,24 +115,3 @@ cdef class MinuteSimulationClock:
yield minute, BAR yield minute, BAR
if minute_emission: if minute_emission:
yield minute, MINUTE_END yield minute, MINUTE_END
cdef class FiveMinuteSimulationClock(MinuteSimulationClock):
@cython.boundscheck(False)
@cython.wraparound(False)
cdef dict calc_minutes_by_session(self):
cdef dict five_minutes_by_session
cdef int session_idx
cdef np.int64_t session_nano
cdef np.ndarray[np.int64_t, ndim=1] five_minutes_nanos
five_minutes_by_session = {}
for session_idx, session_nano in enumerate(self.sessions_nanos):
five_minutes_nanos = np.arange(
self.market_opens_nanos[session_idx],
self.market_closes_nanos[session_idx],
_nanos_in_five_minutes
)
five_minutes_by_session[session_nano] = pd.to_datetime(
five_minutes_nanos, utc=True, box=True
)
return five_minutes_by_session
+4 -3
View File
@@ -27,14 +27,15 @@ from catalyst.gens.sim_engine import (
BEFORE_TRADING_START_BAR BEFORE_TRADING_START_BAR
) )
log = Logger('Trade Simulation') from catalyst.constants import LOG_LEVEL
log = Logger('Trade Simulation', level=LOG_LEVEL)
class AlgorithmSimulator(object): class AlgorithmSimulator(object):
EMISSION_TO_PERF_KEY_MAP = { EMISSION_TO_PERF_KEY_MAP = {
'minute': 'minute_perf', 'minute': 'minute_perf',
'5-minute': '5_minute_perf',
'daily': 'daily_perf' 'daily': 'daily_perf'
} }
@@ -202,7 +203,7 @@ class AlgorithmSimulator(object):
stack.enter_context(self.processor) stack.enter_context(self.processor)
stack.enter_context(ZiplineAPI(self.algo)) stack.enter_context(ZiplineAPI(self.algo))
if algo.data_frequency in set(('minute', '5-minute')): if algo.data_frequency == 'minute':
def execute_order_cancellation_policy(): def execute_order_cancellation_policy():
algo.blotter.execute_cancel_policy(SESSION_END) algo.blotter.execute_cancel_policy(SESSION_END)
@@ -41,10 +41,6 @@ class CryptoPricingLoader(PipelineLoader):
reader = bundle.daily_bar_reader reader = bundle.daily_bar_reader
all_sessions = cal.all_sessions all_sessions = cal.all_sessions
elif data_frequency == '5-minute':
reader = bundle.five_minute_bar_reader
all_sessions = cal.all_five_minutes
elif data_frequency == '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
@@ -40,8 +40,6 @@ class USEquityPricingLoader(PipelineLoader):
if data_frequency == 'daily': if data_frequency == 'daily':
reader = bundle.daily_bar_reader reader = bundle.daily_bar_reader
elif data_frequency == '5-minute':
reader = bundle.five_minute_bar_reader
elif daily_bar_reader == 'minute': elif daily_bar_reader == 'minute':
reader = bundle.minute_bar_reader reader = bundle.minute_bar_reader
else: else:
@@ -53,9 +51,6 @@ class USEquityPricingLoader(PipelineLoader):
if data_frequency == 'daily': if data_frequency == 'daily':
all_sessions = cal.all_sessions all_sessions = cal.all_sessions
elif data_frequency == '5-minute':
reader = bundle.five_minute_bar_reader
all_sessions = cal.all_five_minutes
elif daily_bar_reader == 'minute': elif daily_bar_reader == 'minute':
reader = bundle.minute_bar_reader reader = bundle.minute_bar_reader
all_sessions = cal.all_minutes all_sessions = cal.all_minutes
+7 -29
View File
@@ -65,19 +65,6 @@ class BenchmarkSource(object):
) )
self._precalculated_series = minute_series self._precalculated_series = minute_series
elif self.emission_rate == '5-minute':
five_minutes = \
trading_calendar.five_minutes_for_sessions_in_range(
sessions[0],
sessions[-1],
)
five_minute_series = daily_series.reindex(
index=five_minutes,
method='ffill',
)
self._precalculated_series = five_minute_series
else: else:
self._precalculated_series = daily_series self._precalculated_series = daily_series
else: else:
@@ -85,7 +72,13 @@ class BenchmarkSource(object):
"benchmark_returns.") "benchmark_returns.")
def get_value(self, dt): def get_value(self, dt):
return self._precalculated_series.loc[dt] try:
series = self._precalculated_series
value = series.loc[dt]
return value
except Exception:
# TODO: workaround, find permanent fix
return 0
def get_range(self, start_dt, end_dt): def get_range(self, start_dt, end_dt):
return self._precalculated_series.loc[start_dt:end_dt] return self._precalculated_series.loc[start_dt:end_dt]
@@ -168,21 +161,6 @@ class BenchmarkSource(object):
ffill=True ffill=True
)[asset] )[asset]
return benchmark_series.pct_change()[1:]
elif self.emission_rate == '5-minute':
five_minutes = trading_calendar.five_minutes_for_sessions_in_range(
self.sessions[0], self.sessions[-1]
)
benchmark_series = data_portal.get_history_window(
[asset],
five_minutes[-1],
bar_count=len(five_minutes) + 1,
frequency='5m',
field='price',
data_frequency=self.emission_rate,
ffill=True,
)[asset]
return benchmark_series.pct_change()[1:] return benchmark_series.pct_change()[1:]
else: else:
start_date = asset.start_date start_date = asset.start_date
+3 -1
View File
@@ -23,7 +23,9 @@ from catalyst.protocol import (
) )
from catalyst.assets import Equity from catalyst.assets import Equity
logger = Logger('Requests Source Logger') from catalyst.constants import LOG_LEVEL
logger = Logger('Requests Source Logger', level=LOG_LEVEL)
def roll_dts_to_midnight(dts, trading_day): def roll_dts_to_midnight(dts, trading_day):
View File
+109
View File
@@ -0,0 +1,109 @@
import pandas as pd
from catalyst import run_algorithm
from catalyst.exchange.exchange_utils import get_exchange_symbols
from catalyst.api import (
symbols,
)
def initialize(context):
context.i = -1
context.base_currency = 'btc'
def handle_data(context, data):
lookback = 60 * 24 * 7 # (minutes, hours, days)
context.i += 1
if context.i < lookback:
return
today = context.blotter.current_dt.strftime('%Y-%m-%d %H:%M:%S')
try:
# update universe everyday
new_day = 60 * 24
if not context.i % new_day:
context.universe = universe(context, today)
# get data every 30 minutes
minutes = 30
if not context.i % minutes and context.universe:
for coin in context.coins:
pair = str(coin.symbol)
# ohlcv data
open = data.history(coin, 'open', lookback,
'1m').ffill().bfill().resample(
'30T').first()
high = data.history(coin, 'high', lookback,
'1m').ffill().bfill().resample('30T').max()
low = data.history(coin, 'low', lookback,
'1m').ffill().bfill().resample('30T').min()
close = data.history(coin, 'price', lookback,
'1m').ffill().bfill().resample(
'30T').last()
volume = data.history(coin, 'volume', lookback,
'1m').ffill().bfill().resample(
'30T').sum()
print(today, pair, close[-1])
except Exception as e:
print(e)
def analyze(context=None, results=None):
pass
def universe(context, today):
json_symbols = get_exchange_symbols('poloniex')
poloniex_universe_df = pd.DataFrame.from_dict(
json_symbols).transpose().astype(str)
poloniex_universe_df['base_currency'] = poloniex_universe_df.apply(
lambda row: row.symbol.split('_')[1],
axis=1)
poloniex_universe_df['market_currency'] = poloniex_universe_df.apply(
lambda row: row.symbol.split('_')[0],
axis=1)
poloniex_universe_df = poloniex_universe_df[
poloniex_universe_df['base_currency'] == context.base_currency]
poloniex_universe_df = poloniex_universe_df[
poloniex_universe_df.symbol != 'gas_btc']
# Markets currently not working on Catalyst 0.3.1
# 2017-01-01
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'bcn_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'burst_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'dgb_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'doge_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'emc2_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'pink_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'sc_btc']
print(poloniex_universe_df.head())
date = str(today).split(' ')[0]
poloniex_universe_df = poloniex_universe_df[
poloniex_universe_df.start_date < date]
context.coins = symbols(*poloniex_universe_df.symbol)
print(len(poloniex_universe_df))
return poloniex_universe_df.symbol.tolist()
if __name__ == '__main__':
start_date = pd.to_datetime('2017-01-01', utc=True)
end_date = pd.to_datetime('2017-10-15', utc=True)
performance = run_algorithm(start=start_date, end=end_date,
capital_base=10000.0,
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='poloniex',
data_frequency='minute',
base_currency='btc',
live=False,
live_graph=False,
algo_namespace='test')
+139
View File
@@ -0,0 +1,139 @@
"""
Requires Catalyst version 0.3.0 or above
Tested on Catalyst version 0.3.3
These example aims to provide and easy way for users to learn how to collect data from the different exchanges.
You simply need to specify the exchange and the market that you want to focus on.
You will all see how to create a universe and filter it base on the exchange and the market you desire.
The example prints out the closing price of all the pairs for a given market-exchange every 30 minutes.
The example also contains the ohlcv minute data for the past seven days which could be used to create indicators
Use this as the backbone to create your own trading strategies.
Variables lookback date and date are used to ensure data for a coin existed on the lookback period specified.
"""
import numpy as np
import pandas as pd
from datetime import timedelta
from catalyst import run_algorithm
from catalyst.exchange.exchange_utils import get_exchange_symbols
from catalyst.api import (
symbols,
)
def initialize(context):
context.i = -1 # counts the minutes
context.exchange = 'poloniex' # must match the exchange specified in run_algorithm
context.base_currency = 'btc' # must match the base currency specified in run_algorithm
def handle_data(context, data):
lookback = 60 * 24 * 7 # (minutes, hours, days) of how far to lookback in the data history
context.i += 1
# current date formatted into a string
today = context.blotter.current_dt
date, time = today.strftime('%Y-%m-%d %H:%M:%S').split(' ')
lookback_date = today - timedelta(days=(
lookback / (60 * 24))) # subtract the amount of days specified in lookback
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[
0] # get only the date as a string
# update universe everyday
new_day = 60 * 24
if not context.i % new_day:
context.universe = universe(context, lookback_date, date)
# get data every 30 minutes
minutes = 30
if not context.i % minutes and context.universe:
# we iterate for every pair in the current universe
for coin in context.coins:
pair = str(coin.symbol)
# 30 minute interval ohlcv data (the standard data required for candlestick or indicators/signals)
# 30T means 30 minutes re-sampling of one minute data. change to your desire time interval.
opened = fill(data.history(coin, 'open', bar_count=lookback,
frequency='30T')).values
high = fill(data.history(coin, 'high', bar_count=lookback,
frequency='30T')).values
low = fill(data.history(coin, 'low', bar_count=lookback,
frequency='30T')).values
close = fill(data.history(coin, 'price', bar_count=lookback,
frequency='30T')).values
volume = fill(data.history(coin, 'volume', bar_count=lookback,
frequency='30T')).values
# close[-1] is the equivalent to current price
# displays the minute price for each pair every 30 minutes
print(
today, pair, opened[-1], high[-1], low[-1], close[-1], volume[-1])
# ----------------------------------------------------------------------------------------------------------
# -------------------------------------- Insert Your Strategy Here -----------------------------------------
# ----------------------------------------------------------------------------------------------------------
def analyze(context=None, results=None):
pass
# Get the universe for a given exchange and a given base_currency market
# Example: Poloniex btc Market
def universe(context, lookback_date, current_date):
json_symbols = get_exchange_symbols(
context.exchange) # get all the pairs for the exchange
universe_df = pd.DataFrame.from_dict(json_symbols).transpose().astype(
str) # convert into a dataframe
universe_df['base_currency'] = universe_df.apply(
lambda row: row.symbol.split('_')[1],
axis=1)
universe_df['market_currency'] = universe_df.apply(
lambda row: row.symbol.split('_')[0],
axis=1)
# Filter all the exchange pairs to only the ones for a give base currency
universe_df = universe_df[
universe_df['base_currency'] == context.base_currency]
# Filter all the pairs to ensure that pair existed in the current date range
universe_df = universe_df[universe_df.start_date < lookback_date]
universe_df = universe_df[universe_df.end_daily >= current_date]
context.coins = symbols(
*universe_df.symbol) # convert all the pairs to symbols
return universe_df.symbol.tolist()
# Replace all NA, NAN or infinite values with its nearest value
def fill(series):
if isinstance(series, pd.Series):
return series.replace([np.inf, -np.inf], np.nan).ffill().bfill()
elif isinstance(series, np.ndarray):
return pd.Series(series).replace([np.inf, -np.inf],
np.nan).ffill().bfill().values
else:
return series
if __name__ == '__main__':
start_date = pd.to_datetime('2017-01-08', utc=True)
end_date = pd.to_datetime('2017-11-13', utc=True)
performance = run_algorithm(start=start_date, end=end_date,
capital_base=10000.0,
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='poloniex',
data_frequency='minute',
base_currency='btc',
live=False,
live_graph=False,
algo_namespace='simple_universe')
"""
Run in Terminal (inside catalyst environment):
python simple_universe.py
"""
+42
View File
@@ -0,0 +1,42 @@
import talib
import pandas as pd
from catalyst import run_algorithm
from catalyst.api import symbol
def initialize(context):
print('initializing')
context.asset = symbol('xcp_btc')
def handle_data(context, data):
print('handling bar: {}'.format(data.current_dt))
price = data.current(context.asset, 'close')
print('got price {price}'.format(price=price))
try:
prices = data.history(
context.asset,
fields='close',
bar_count=1,
frequency='1D'
)
print('got {} price entries\n'.format(len(prices), prices))
except Exception as e:
print(e)
run_algorithm(
capital_base=1,
start=pd.to_datetime('2015-3-2', utc=True),
end=pd.to_datetime('2017-8-31', utc=True),
data_frequency='daily',
initialize=initialize,
handle_data=handle_data,
analyze=None,
exchange_name='poloniex',
algo_namespace='issue_55',
base_currency='btc'
)
+46
View File
@@ -0,0 +1,46 @@
import talib
import pandas as pd
from catalyst import run_algorithm
from catalyst.api import symbol
def initialize(context):
print('initializing')
context.asset = symbol('btc_usdt')
def handle_data(context, data):
print('handling bar: {}'.format(data.current_dt))
price = data.current(context.asset, 'close')
print('got price {price}'.format(price=price))
try:
prices = data.history(
context.asset,
fields='close',
bar_count=60,
frequency='1D'
)
print('got {} price entries\n'.format(len(prices), prices))
except Exception as e:
print(e)
run_algorithm(
capital_base=1,
start=pd.to_datetime('2016-2-11', utc=True),
end=pd.to_datetime('2017-8-31', utc=True),
data_frequency='daily',
initialize=initialize,
handle_data=handle_data,
analyze=None,
exchange_name='bittrex',
algo_namespace='issue_57',
base_currency='btc'
<<<<<<< HEAD
)
=======
)
>>>>>>> develop
+127
View File
@@ -0,0 +1,127 @@
from __future__ import division
import os
import pytz
import numpy as np
import pandas as pd
from scipy.optimize import minimize
import matplotlib.pyplot as plt
from datetime import datetime
from catalyst.api import record, symbol, symbols, order_target_percent
from catalyst.utils.run_algo import run_algorithm
np.set_printoptions(threshold='nan', suppress=True)
def initialize(context):
# Portfolio assets list
context.assets = symbols('btc_usdt', 'eth_usdt', 'ltc_usdt', 'dash_usdt',
'xmr_usdt')
context.nassets = len(context.assets)
# Set the time window that will be used to compute expected return
# and asset correlations
context.window = 180
# Set the number of days between each portfolio rebalancing
context.rebalance_period = 30
context.i = 0
def handle_data(context, data):
# Only rebalance at the beggining of the algorithm execution and
# every multiple of the rebalance period
if context.i == 0 or context.i % context.rebalance_period == 0:
n = context.window
prices = data.history(context.assets, fields='price',
bar_count=n + 1, frequency='daily')
pr = np.asmatrix(prices)
t_prices = prices.iloc[1:n + 1]
t_val = t_prices.values
tminus_prices = prices.iloc[0:n]
tminus_val = tminus_prices.values
# Compute daily returns (r)
r = np.asmatrix(t_val / tminus_val - 1)
# Compute the expected returns of each asset with the average
# daily return for the selected time window
m = np.asmatrix(np.mean(r, axis=0))
# ###
stds = np.std(r, axis=0)
# Compute excess returns matrix (xr)
xr = r - m
# Matrix algebra to get variance-covariance matrix
cov_m = np.dot(np.transpose(xr), xr) / n
# Compute asset correlation matrix (informative only)
corr_m = cov_m / np.dot(np.transpose(stds), stds)
# Define portfolio optimization parameters
n_portfolios = 50000
results_array = np.zeros((3 + context.nassets, n_portfolios))
for p in xrange(n_portfolios):
weights = np.random.random(context.nassets)
weights /= np.sum(weights)
w = np.asmatrix(weights)
p_r = np.sum(np.dot(w, np.transpose(m))) * 365
p_std = np.sqrt(
np.dot(np.dot(w, cov_m), np.transpose(w))) * np.sqrt(365)
# store results in results array
results_array[0, p] = p_r
results_array[1, p] = p_std
# store Sharpe Ratio (return / volatility) - risk free rate element
# excluded for simplicity
results_array[2, p] = results_array[0, p] / results_array[1, p]
i = 0
for iw in weights:
results_array[3 + i, p] = weights[i]
i += 1
# convert results array to Pandas DataFrame
results_frame = pd.DataFrame(np.transpose(results_array),
columns=['r', 'stdev',
'sharpe'] + context.assets)
# locate position of portfolio with highest Sharpe Ratio
max_sharpe_port = results_frame.iloc[results_frame['sharpe'].idxmax()]
# locate positon of portfolio with minimum standard deviation
min_vol_port = results_frame.iloc[results_frame['stdev'].idxmin()]
# order optimal weights for each asset
for asset in context.assets:
if data.can_trade(asset):
order_target_percent(asset, max_sharpe_port[asset])
# create scatter plot coloured by Sharpe Ratio
plt.scatter(results_frame.stdev, results_frame.r,
c=results_frame.sharpe, cmap='RdYlGn')
plt.xlabel('Volatility')
plt.ylabel('Returns')
plt.colorbar()
# plot red star to highlight position of portfolio with highest Sharpe Ratio
plt.scatter(max_sharpe_port[1], max_sharpe_port[0], marker='o',
color='b', s=200)
# plot green star to highlight position of minimum variance portfolio
plt.show()
print(max_sharpe_port)
record(pr=pr, r=r, m=m, stds=stds, max_sharpe_port=max_sharpe_port,
corr_m=corr_m)
context.i += 1
def analyze(context=None, results=None):
# Form DataFrame with selected data
data = results[['pr', 'r', 'm', 'stds', 'max_sharpe_port', 'corr_m',
'portfolio_value']]
# Save results in CSV file
filename = os.path.splitext(os.path.basename(__file__))[0]
data.to_csv(filename + '.csv')
# Bitcoin data is available from 2015-3-2. Dates vary for other tokens.
start = datetime(2017, 1, 1, 0, 0, 0, 0, pytz.utc)
end = datetime(2017, 8, 16, 0, 0, 0, 0, pytz.utc)
results = run_algorithm(initialize=initialize,
handle_data=handle_data,
analyze=analyze,
start=start,
end=end,
exchange_name='poloniex',
capital_base=100000, )
+153
View File
@@ -0,0 +1,153 @@
import pandas as pd
from logbook import Logger, DEBUG
from catalyst import run_algorithm
from catalyst.api import (schedule_function, order_target_percent, symbol,
date_rules, get_open_orders, cancel_order, record,
set_commission, set_slippage)
log = Logger('rodrigo_1', level=DEBUG)
"""
The initialize function sets any data or variables that
you'll use in your algorithm.
It's only called once at the beginning of your algorithm.
"""
def initialize(context):
# Select asset of interest
context.asset = symbol('BTC_USD')
# set_commission(TradingPairFeeSchedule(maker_fee=0.5, taker_fee=0.5))
# set_slippage(TradingPairFixedSlippage(spread=0.5))
# Set up a rebalance method to run every day
schedule_function(rebalance, date_rule=date_rules.every_day())
"""
Rebalance function scheduled to run once per day.
"""
def rebalance(context, data):
# To make market decisions, we're calculating the token's
# moving average for the last 5 days.
# We get the price history for the last 5 days.
price_history = data.history(context.asset, fields='price', bar_count=5,
frequency='1d')
# Then we take an average of those 5 days.
average_price = price_history.mean()
# We also get the coin's current price.
price = data.current(context.asset, 'price')
# Cancel any outstanding orders
orders = get_open_orders(context.asset) or []
for order in orders:
cancel_order(order)
# If our coin is currently listed on a major exchange
if data.can_trade(context.asset):
# If the current price is 1% above the 5-day average price,
# we open a long position. If the current price is below the
# average price, then we want to close our position to 0 shares.
if price > (1.01 * average_price):
# Place the buy order (positive means buy, negative means sell)
order_target_percent(context.asset, .99)
log.info("Buying %s" % (context.asset.symbol))
elif price < average_price:
# Sell all of our shares by setting the target position to zero
order_target_percent(context.asset, 0)
log.info("Selling %s" % (context.asset.symbol))
# Use the record() method to track up to five custom signals.
# Record Apple's current price and the average price over the last
# five days.
cash = context.portfolio.cash
leverage = context.account.leverage
record(price=price, average_price=average_price, cash=cash,
leverage=leverage)
def analyze(context=None, results=None):
import matplotlib.pyplot as plt
# Plot the portfolio and asset data.
ax1 = plt.subplot(511)
results[['portfolio_value']].plot(ax=ax1)
ax1.set_ylabel('Portfolio Value (USD)')
ax2 = plt.subplot(512, sharex=ax1)
ax2.set_ylabel('{asset} (USD)'.format(asset=context.asset))
(results[[
'price',
]]).plot(ax=ax2)
trans = results.ix[[t != [] for t in results.transactions]]
buys = trans.ix[
[t[0]['amount'] > 0 for t in trans.transactions]
]
sells = trans.ix[
[t[0]['amount'] < 0 for t in trans.transactions]
]
ax2.plot(
buys.index,
results.price[buys.index],
'^',
markersize=10,
color='g',
)
ax2.plot(
sells.index,
results.price[sells.index],
'v',
markersize=10,
color='r',
)
ax3 = plt.subplot(513, sharex=ax1)
results[['leverage']].plot(ax=ax3)
ax3.set_ylabel('Leverage ')
ax4 = plt.subplot(514, sharex=ax1)
results[['cash']].plot(ax=ax4)
ax4.set_ylabel('Cash (USD)')
results[[
'algorithm',
'benchmark',
]] = results[[
'algorithm_period_return',
'benchmark_period_return',
]]
ax5 = plt.subplot(515, sharex=ax1)
results[[
'algorithm',
'benchmark',
]].plot(ax=ax5)
ax5.set_ylabel('Percent Change')
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
run_algorithm(
capital_base=100000,
start=pd.to_datetime('2017-1-1', utc=True),
end=pd.to_datetime('2017-10-22', utc=True),
data_frequency='minute',
initialize=initialize,
handle_data=None,
analyze=analyze,
exchange_name='bitfinex',
algo_namespace='rodrigo_1',
base_currency='usd'
)
@@ -31,4 +31,4 @@ class OpenExchangeCalendar(TradingCalendar):
return DateOffset(days=1) return DateOffset(days=1)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(OpenExchangeCalendar, self).__init__(start=Timestamp('2015-03-01', tz='UTC'), **kwargs) super(OpenExchangeCalendar, self).__init__(start=Timestamp('2015-3-1', tz='UTC'), **kwargs)
@@ -118,9 +118,6 @@ class TradingCalendar(with_metaclass(ABCMeta)):
self._trading_minutes_nanos = self.all_minutes.values.\ self._trading_minutes_nanos = self.all_minutes.values.\
astype(np.int64) astype(np.int64)
self._trading_five_minutes_nanos = self.all_five_minutes.values.\
astype(np.int64)
self.first_trading_session = _all_days[0] self.first_trading_session = _all_days[0]
self.last_trading_session = _all_days[-1] self.last_trading_session = _all_days[-1]
@@ -182,18 +179,6 @@ class TradingCalendar(with_metaclass(ABCMeta)):
""" """
return int(self._minutes_per_session[start_session:end_session].sum()) return int(self._minutes_per_session[start_session:end_session].sum())
@lazyval
def _five_minutes_per_session(self):
diff = self.schedule.market_close - self.schedule.market_open
diff = diff.astype('timedelta64[m]')
return (diff + 1) // 5
def five_minutes_count_for_sessions_in_range(self,
start_session,
end_session):
five_mins = self._five_minutes_per_session[start_session:end_session]
return int(five_mins.sum())
@property @property
def regular_holidays(self): def regular_holidays(self):
""" """
@@ -386,10 +371,6 @@ class TradingCalendar(with_metaclass(ABCMeta)):
idx = next_divider_idx(self._trading_minutes_nanos, dt.value) idx = next_divider_idx(self._trading_minutes_nanos, dt.value)
return self.all_minutes[idx] return self.all_minutes[idx]
def next_five_minute(self, dt):
idx = next_divider_idx(self._trading_five_minutes_nanos, dt.values)
return self.all_five_mintutes[idx]
def previous_minute(self, dt): def previous_minute(self, dt):
""" """
Given a dt, return the previous exchange minute. Given a dt, return the previous exchange minute.
@@ -484,12 +465,6 @@ class TradingCalendar(with_metaclass(ABCMeta)):
end_minute=self.schedule.at[session_label, 'market_close'], end_minute=self.schedule.at[session_label, 'market_close'],
) )
def five_minutes_for_session(self, session_label):
return self.five_minutes_in_range(
start_five_minute=self.schedule.at[session_label, 'market_open'],
end_five_minute=self.schedule.at[session_label, 'market_close'],
)
def minutes_window(self, start_dt, count): def minutes_window(self, start_dt, count):
start_dt_nanos = start_dt.value start_dt_nanos = start_dt.value
all_minutes_nanos = self._trading_minutes_nanos all_minutes_nanos = self._trading_minutes_nanos
@@ -591,20 +566,6 @@ class TradingCalendar(with_metaclass(ABCMeta)):
return abs(end_idx - start_idx) return abs(end_idx - start_idx)
def five_minutes_in_range(self, start_five_minute, end_five_minute):
start_idx = searchsorted(self._trading_five_minutes_nanos,
start_five_minute.value)
end_idx = searchsorted(self._trading_five_minutes_nanos,
end_five_minute.value)
if end_five_minute.value == self._trading_five_minutes_nanos[end_idx]:
# if the end minute is a market minute, increase by 1
end_idx += 1
return self.all_five_minutes[start_idx:end_idx]
def minutes_in_range(self, start_minute, end_minute): def minutes_in_range(self, start_minute, end_minute):
""" """
Given start and end minutes, return all the calendar minutes Given start and end minutes, return all the calendar minutes
@@ -662,15 +623,6 @@ class TradingCalendar(with_metaclass(ABCMeta)):
return self.minutes_in_range(first_minute, last_minute) return self.minutes_in_range(first_minute, last_minute)
def five_minutes_for_sessions_in_range(self,
start_session_label,
end_session_label):
first_minute, _ = self.open_and_close_for_session(start_session_label)
_, last_minute = self.open_and_close_for_session(end_session_label)
return self.five_minutes_in_range(first_minute, last_minute)
def open_and_close_for_session(self, session_label): def open_and_close_for_session(self, session_label):
""" """
Returns a tuple of timestamps of the open and close of the session Returns a tuple of timestamps of the open and close of the session
@@ -777,13 +729,6 @@ class TradingCalendar(with_metaclass(ABCMeta)):
return DatetimeIndex(all_minutes).tz_localize("UTC") return DatetimeIndex(all_minutes).tz_localize("UTC")
@lazyval
def all_five_minutes(self):
"""
Returns a DatetimeIndex representing all the five minutes in this calendar.
"""
return self._all_minutes_with_interval(5)
@lazyval @lazyval
def all_minutes(self): def all_minutes(self):
""" """
-1
View File
@@ -602,7 +602,6 @@ class date_rules(object):
class time_rules(object): class time_rules(object):
market_open = AfterOpen market_open = AfterOpen
market_close = BeforeClose market_close = BeforeClose
every_5_minutes = Always
every_minute = Always every_minute = Always
+1 -1
View File
@@ -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 = expanduser('~/.catalyst') root = os.path.join(expanduser('~'),'.catalyst')
return root return root
+155 -120
View File
@@ -1,16 +1,19 @@
import os import os
import re import re
from runpy import run_path
import sys import sys
import warnings import warnings
from time import sleep
from datetime import timedelta from datetime import timedelta
from runpy import run_path
import pandas as pd from time import sleep
import click import click
import pandas as pd
from catalyst.data.bundles import load
from catalyst.data.data_portal import DataPortal
from catalyst.exchange.bittrex.bittrex import Bittrex from catalyst.exchange.bittrex.bittrex import Bittrex
from catalyst.exchange.bitfinex.bitfinex import Bitfinex
from catalyst.exchange.poloniex.poloniex import Poloniex
try: try:
from pygments import highlight from pygments import highlight
@@ -23,34 +26,29 @@ except:
from toolz import valfilter, concatv from toolz import valfilter, concatv
from functools import partial from functools import partial
from catalyst.algorithm import TradingAlgorithm
from catalyst.data.bundles.core import load
from catalyst.data.data_portal import DataPortal
from catalyst.data.loader import load_crypto_market_data
from catalyst.finance.trading import TradingEnvironment from catalyst.finance.trading import TradingEnvironment
from catalyst.pipeline.data import USEquityPricing, CryptoPricing
from catalyst.pipeline.loaders import (
USEquityPricingLoader,
CryptoPricingLoader,
)
from catalyst.utils.calendars import get_calendar from catalyst.utils.calendars import get_calendar
from catalyst.utils.factory import create_simulation_parameters from catalyst.utils.factory import create_simulation_parameters
from catalyst.data.loader import load_crypto_market_data
import catalyst.utils.paths as pth import catalyst.utils.paths as pth
from catalyst.exchange.algorithm_exchange import ExchangeTradingAlgorithm from catalyst.exchange.exchange_algorithm import ExchangeTradingAlgorithmLive, \
from catalyst.exchange.data_portal_exchange import DataPortalExchange ExchangeTradingAlgorithmBacktest
from catalyst.exchange.bitfinex.bitfinex import Bitfinex from catalyst.exchange.exchange_data_portal import DataPortalExchangeLive, \
DataPortalExchangeBacktest
from catalyst.exchange.asset_finder_exchange import AssetFinderExchange from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
from catalyst.exchange.exchange_portfolio import ExchangePortfolio from catalyst.exchange.exchange_portfolio import ExchangePortfolio
from catalyst.exchange.exchange_errors import ( from catalyst.exchange.exchange_errors import (
ExchangeRequestError, ExchangeRequestError, ExchangeAuthEmpty,
ExchangeRequestErrorTooManyAttempts, ExchangeRequestErrorTooManyAttempts,
BaseCurrencyNotFoundError) BaseCurrencyNotFoundError, ExchangeNotFoundError)
from catalyst.exchange.exchange_utils import get_exchange_auth, \ from catalyst.exchange.exchange_utils import get_exchange_auth, \
get_algo_object get_algo_object, get_exchange_folder
from logbook import Logger from logbook import Logger
log = Logger('run_algo') from catalyst.constants import LOG_LEVEL
log = Logger('run_algo', level=LOG_LEVEL)
class _RunAlgoError(click.ClickException, ValueError): class _RunAlgoError(click.ClickException, ValueError):
@@ -148,72 +146,98 @@ def _run(handle_data,
mode = 'live' if live else 'backtest' mode = 'live' if live else 'backtest'
log.info('running algo in {mode} mode'.format(mode=mode)) log.info('running algo in {mode} mode'.format(mode=mode))
if live and exchange is not None:
exchange_name = exchange exchange_name = exchange
start = pd.Timestamp.utcnow() if exchange_name is None:
end = start + timedelta(minutes=1439) raise ValueError('Please specify at least one exchange.')
exchange_list = [x.strip().lower() for x in exchange.split(',')]
exchanges = dict()
for exchange_name in exchange_list:
# Looking for the portfolio from the cache first
portfolio = get_algo_object( portfolio = get_algo_object(
algo_name=algo_namespace, algo_name=algo_namespace,
key='portfolio_{}'.format(exchange_name), key='portfolio_{}'.format(exchange_name),
environ=environ environ=environ
) )
if portfolio is None: if portfolio is None:
portfolio = ExchangePortfolio( portfolio = ExchangePortfolio(
start_date=pd.Timestamp.utcnow() start_date=pd.Timestamp.utcnow()
) )
# This corresponds to the json file containing api token info
exchange_auth = get_exchange_auth(exchange_name) exchange_auth = get_exchange_auth(exchange_name)
if live and (exchange_auth['key'] == '' \
or exchange_auth['secret'] == ''):
raise ExchangeAuthEmpty(
exchange=exchange_name.title(),
filename=os.path.join(
get_exchange_folder(exchange_name, environ), 'auth.json'))
if exchange_name == 'bitfinex': if exchange_name == 'bitfinex':
exchange = Bitfinex( exchanges[exchange_name] = Bitfinex(
key=exchange_auth['key'], key=exchange_auth['key'],
secret=exchange_auth['secret'], secret=exchange_auth['secret'],
base_currency=base_currency, base_currency=base_currency,
portfolio=portfolio portfolio=portfolio
) )
elif exchange_name == 'bittrex': elif exchange_name == 'bittrex':
exchange = 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'], key=exchange_auth['key'],
secret=exchange_auth['secret'], secret=exchange_auth['secret'],
base_currency=base_currency, base_currency=base_currency,
portfolio=portfolio portfolio=portfolio
) )
else: else:
raise NotImplementedError( raise ExchangeNotFoundError(exchange_name=exchange_name)
'exchange not supported: %s' % exchange_name)
open_calendar = get_calendar('OPEN') open_calendar = get_calendar('OPEN')
sim_params = create_simulation_parameters(
start=start,
end=end,
capital_base=capital_base,
data_frequency=data_frequency,
emission_rate=data_frequency,
)
if live and exchange is not None:
env = TradingEnvironment( env = TradingEnvironment(
load=partial(
load_crypto_market_data,
environ=environ,
start_dt=start,
end_dt=end
),
environ=environ, environ=environ,
exchange_tz='UTC', exchange_tz='UTC',
asset_db_path=None asset_db_path=None # We don't need an asset db, we have exchanges
) )
env.asset_finder = AssetFinderExchange(exchange) env.asset_finder = AssetFinderExchange()
choose_loader = None # TODO: use the DataPortal for in the algorithm class for this
data = DataPortalExchange( if live:
exchange=exchange, start = pd.Timestamp.utcnow()
# TODO: fix the end data.
end = start + timedelta(hours=8760)
data = DataPortalExchangeLive(
exchanges=exchanges,
asset_finder=env.asset_finder, asset_finder=env.asset_finder,
trading_calendar=open_calendar, trading_calendar=open_calendar,
first_trading_day=pd.to_datetime('today', utc=True) first_trading_day=pd.to_datetime('today', utc=True)
) )
choose_loader = None
def fetch_capital_base(attempt_index=0): def fetch_capital_base(exchange, attempt_index=0):
""" """
Fetch the base currency amount required to bootstrap Fetch the base currency amount required to bootstrap
the algorithm against the exchange. the algorithm against the exchange.
The algorithm cannot continue without this value. The algorithm cannot continue without this value.
:param exchange: the targeted exchange
:param attempt_index: :param attempt_index:
:return capital_base: the amount of base currency available for :return capital_base: the amount of base currency available for
trading trading
@@ -224,8 +248,14 @@ def _run(handle_data,
balances = exchange.get_balances() balances = exchange.get_balances()
except ExchangeRequestError as e: except ExchangeRequestError as e:
if attempt_index < 20: if attempt_index < 20:
log.warn(
'could not retrieve balances on {}: {}'.format(
exchange.name, e
)
)
sleep(5) sleep(5)
return fetch_capital_base(attempt_index + 1) return fetch_capital_base(exchange, attempt_index + 1)
else: else:
raise ExchangeRequestErrorTooManyAttempts( raise ExchangeRequestErrorTooManyAttempts(
attempts=attempt_index, attempts=attempt_index,
@@ -233,35 +263,86 @@ def _run(handle_data,
) )
if base_currency in balances: if base_currency in balances:
return balances[base_currency] base_currency_available = balances[base_currency]
log.info(
'base currency available in the account: {} {}'.format(
base_currency_available, base_currency
)
)
if capital_base is not None \
and capital_base < base_currency_available:
log.info(
'using capital base limit: {} {}'.format(
capital_base, base_currency
)
)
amount = capital_base
else:
amount = base_currency_available
return amount
else: else:
raise BaseCurrencyNotFoundError( raise BaseCurrencyNotFoundError(
base_currency=base_currency, base_currency=base_currency,
exchange=exchange_name exchange=exchange_name
) )
combined_capital_base = 0
for exchange_name in exchanges:
exchange = exchanges[exchange_name]
combined_capital_base += fetch_capital_base(exchange)
sim_params = create_simulation_parameters( sim_params = create_simulation_parameters(
start=start, start=start,
end=end, end=end,
capital_base=fetch_capital_base(), capital_base=capital_base,
emission_rate='minute', emission_rate='minute',
data_frequency='minute' data_frequency='minute'
) )
# TODO: use the constructor instead
sim_params._arena = 'live'
algorithm_class = partial(
ExchangeTradingAlgorithmLive,
exchanges=exchanges,
algo_namespace=algo_namespace,
live_graph=live_graph
)
elif exchanges:
# Removed the existing Poloniex fork to keep things simple
# We can add back the complexity if required.
# I don't think that we should have arbitrary price data bundles
# Instead, we should center this data around exchanges.
# We still need to support bundles for other misc data, but we
# can handle this later.
data = DataPortalExchangeBacktest(
exchange_names=[exchange_name for exchange_name in exchanges],
asset_finder=None,
trading_calendar=open_calendar,
first_trading_day=start,
last_available_session=end
)
sim_params = create_simulation_parameters(
start=start,
end=end,
capital_base=capital_base,
data_frequency=data_frequency,
emission_rate=data_frequency,
)
algorithm_class = partial(
ExchangeTradingAlgorithmBacktest,
exchanges=exchanges
)
elif bundle is not None: elif bundle is not None:
bundles = bundle.split(',')
def get_trading_env_and_data(bundles):
env = data = None
b = 'poloniex'
if len(bundles) == 0:
return env, data
elif len(bundles) == 1:
b = bundles[0]
bundle_data = load( bundle_data = load(
b, bundle,
environ, environ,
bundle_timestamp, bundle_timestamp,
) )
@@ -277,73 +358,19 @@ def _run(handle_data,
str(bundle_data.asset_finder.engine.url), str(bundle_data.asset_finder.engine.url),
) )
env = TradingEnvironment( env = TradingEnvironment(asset_db_path=connstr, environ=environ)
load=partial(load_crypto_market_data, bundle=b, first_trading_day = \
bundle_data=bundle_data, environ=environ), bundle_data.equity_minute_bar_reader.first_trading_day
bm_symbol='USDT_BTC',
trading_calendar=open_calendar,
asset_db_path=connstr,
environ=environ,
)
first_trading_day = bundle_data.minute_bar_reader.first_trading_day
data = DataPortal( data = DataPortal(
env.asset_finder, env.asset_finder, open_calendar,
open_calendar,
first_trading_day=first_trading_day, first_trading_day=first_trading_day,
minute_reader=bundle_data.minute_bar_reader, equity_minute_reader=bundle_data.equity_minute_bar_reader,
five_minute_reader=bundle_data.five_minute_bar_reader, equity_daily_reader=bundle_data.equity_daily_bar_reader,
daily_reader=bundle_data.daily_bar_reader,
adjustment_reader=bundle_data.adjustment_reader, adjustment_reader=bundle_data.adjustment_reader,
) )
return env, data perf = algorithm_class(
def get_loader_for_bundle(b):
bundle_data = load(
b,
environ,
bundle_timestamp,
)
if b == 'poloniex':
return CryptoPricingLoader(
bundle_data,
data_frequency,
CryptoPricing,
)
elif b == 'quandl':
return USEquityPricingLoader(
bundle_data,
data_frequency,
USEquityPricing,
)
raise ValueError(
"No PipelineLoader registered for bundle %s." % b
)
loaders = [get_loader_for_bundle(b) for b in bundles]
env, data = get_trading_env_and_data(bundles)
def choose_loader(column):
for loader in loaders:
if column in loader.columns:
return loader
raise ValueError(
"No PipelineLoader registered for column %s." % column
)
else:
env = TradingEnvironment(environ=environ)
choose_loader = None
TradingAlgorithmClass = (
partial(ExchangeTradingAlgorithm, exchange=exchange,
algo_namespace=algo_namespace, live_graph=live_graph)
if live and exchange else TradingAlgorithm)
perf = TradingAlgorithmClass(
namespace=namespace, namespace=namespace,
env=env, env=env,
get_pipeline_loader=choose_loader, get_pipeline_loader=choose_loader,
@@ -442,7 +469,8 @@ def run_algorithm(initialize,
exchange_name=None, exchange_name=None,
base_currency=None, base_currency=None,
algo_namespace=None, algo_namespace=None,
live_graph=False): live_graph=False,
output=os.devnull):
"""Run a trading algorithm. """Run a trading algorithm.
Parameters Parameters
@@ -512,8 +540,15 @@ def run_algorithm(initialize,
-------- --------
catalyst.data.bundles.bundles : The available data bundles. catalyst.data.bundles.bundles : The available data bundles.
""" """
load_extensions(default_extension, extensions, strict_extensions, environ) load_extensions(
default_extension, extensions, strict_extensions, environ
)
# I'm not sure that we need this since the modified DataPortal
# does not require extensions to be explicitly loaded.
# This will be useful for arbitrary non-pricing bundles but we may
# need to modify the logic.
if not live: if not live:
non_none_data = valfilter(bool, { non_none_data = valfilter(bool, {
'data': data is not None, 'data': data is not None,
@@ -548,7 +583,7 @@ def run_algorithm(initialize,
bundle_timestamp=bundle_timestamp, bundle_timestamp=bundle_timestamp,
start=start, start=start,
end=end, end=end,
output=os.devnull, output=output,
print_algo=False, print_algo=False,
local_namespace=False, local_namespace=False,
environ=environ, environ=environ,
+1 -1
View File
@@ -1 +1 @@
www.zipline.io enigma-catalyst.readthedocs.io
-105
View File
@@ -1,105 +0,0 @@
<h1>Live Trading</h1>
This document explains how to get started with live trading.
<h2>Supported Exchanges</h2>
Catalyst can trade against these exchanges:
* Bitfinex, id=`bitfinex`
* Bittrex, id=`bittrex`
<h3>Authentication</h3>
Most exchanges require key/token combination for authentication. By
convention, Catalyst uses an "auth.json" file to hold this data.
This example illustrates the convention using the Bitfinex exchange.
Here is how to generate key and secret values for bitfinex:
https://docs.bitfinex.com/v1/docs/api-access. Most exchanges follow
a similar process.
The auth.json file:
```json
{
"name": "bitfinex",
"key": "my-key",
"secret": "my-secret"
}
```
The file goes here:
```
~/.catalyst/data/exchanges/bitfinex/auth.json
```
Note that the 'bitfinex' directory corresponds to the id of the Bitfinex
exchange as defined in the "Supported Exchanges" section above.
Attempting to run an algorithm where the targeted exchange is missing
its "auth.json" file will create the directory structure but result
in an error.
<h3>Currency Symbols</h3>
Catalyst introduces a universal convention to reference
trading pairs and individual currencies. This
is required to ensure that the `symbol()` api predictably
returns the correct asset regardless of the targeted exchange.
Exchanges tend to use their own convention to represent currencies
(e.g. XBT and BTC both represent Bitcoin on different exchanges).
Trading pairs are also inconsistent. For example, Bitfinex
puts the market currency before the base currency without a
separator, Bittrex puts the base currency first and uses a dash
seperator.
Here is the Catalyst convention:
*[Market Currency]_[Base Currency]* all lowercase.
Currency symbols (e.g. btc, eth, ltc) follow the Bittrex convention.
Here are some examples:
```python
# With Bitfinex
bitcoin_usd_asset = symbol('btc_usd')
ethereum_bitcoin_asset = symbol('eth_btc')
# With Bittrex
ethereum_bitcoin_asset = symbol('eth_btc')
neo_ethereum_asset = symbol('neo_eth)
```
Note that the trading pairs are always referenced in the same manner.
However, not all trading pairs are available on all exchanges. An
error will occur if the specified trading pair is not trading
on the exchange.
<h2>Trading an Algorithm</h2>
There is no special convention to follow when writing an
algorithm for live trading. The same algorithm should work in
backtest and live execution mode without modification.
What differs are the arguments provided to the catalyst client or
`run_algorithm()` interface. Here is example:
```python
run_algorithm(
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
live=True,
algo_namespace='my_algo_trading_xrp',
base_currency='btc'
)
```
Here is the breakdown of the new arguments:
* live: Boolean flag which enables live trading.
* exchange_name: The name of the targeted exchange
(supported values: *bitfinex*, *bittrex*).
* algo_namespace: A arbitrary label assigned to your algorithm for
data storage purposes.
* base_currency: The base currency used to calculate the
statistics of your algorithm. Currently, the base currency of all
trading pairs of your algorithm must match this value.
Here is a complete algorithm for reference:
[Buy Low and Sell High](../catalyst/examples/buy_low_sell_high_live.py)
File diff suppressed because it is too large Load Diff
+11 -10
View File
@@ -1,7 +1,7 @@
import sys import sys
import os import os
from zipline import __version__ as version #from catalyst import __version__ as version
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
@@ -21,14 +21,14 @@ extensions = [
extlinks = { extlinks = {
'issue': ('https://github.com/quantopian/zipline/issues/%s', '#'), 'issue': ('https://github.com/enigmampc/catalyst/issues/%s', '#'),
'commit': ('https://github.com/quantopian/zipline/commit/%s', ''), 'commit': ('https://github.com/enigmampc/catalyst/commit/%s', ''),
} }
# -- Docstrings --------------------------------------------------------------- # -- Docstrings ---------------------------------------------------------------
extensions += ['numpydoc'] #extensions += ['numpydoc']
numpydoc_show_class_members = False #numpydoc_show_class_members = False
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['.templates'] templates_path = ['.templates']
@@ -40,11 +40,12 @@ source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
project = u'Zipline' project = u'Catalyst'
copyright = u'2016, Quantopian Inc.' copyright = u'2017, Enigma MPC, Inc.'
# The full version, including alpha/beta/rc tags, but excluding the commit hash # The full version, including alpha/beta/rc tags, but excluding the commit hash
release = version.split('+', 1)[0] #release = version.split('+', 1)[0]
release = '0.3'
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
@@ -84,7 +85,7 @@ html_show_sphinx = True
html_show_copyright = True html_show_copyright = True
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'ziplinedoc' htmlhelp_basename = 'catalystdoc'
intersphinx_mapping = { intersphinx_mapping = {
'http://docs.python.org/dev': None, 'http://docs.python.org/dev': None,
@@ -93,6 +94,6 @@ intersphinx_mapping = {
'pandas': ('http://pandas.pydata.org/pandas-docs/stable/', None), 'pandas': ('http://pandas.pydata.org/pandas-docs/stable/', None),
} }
doctest_global_setup = "import zipline" doctest_global_setup = "import catalyst"
todo_include_todos = True todo_include_todos = True
+30 -105
View File
@@ -1,21 +1,17 @@
Development Guidelines Development Guidelines
====================== ======================
This page is intended for developers of Zipline, people who want to contribute to the Zipline codebase or documentation, or people who want to install from source and make local changes to their copy of Zipline. This page is intended for developers of Catalyst, people who want to contribute to the Catalyst codebase or documentation, or people who want to install from source and make local changes to their copy of Catalyst.
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We `track issues`__ on `GitHub`__ and also have a `mailing list`__ where you can ask questions. All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We `track issues <https://github.com/enigmampc/catalyst/issues>`_ on `GitHub <https://github.com/enigmampc/catalyst>`_ and also have a `discord group <https://discord.gg/SJK32GY>`_ where you can ask questions.
__ https://github.com/quantopian/zipline/issues
__ https://github.com/
__ https://groups.google.com/forum/#!forum/zipline
Creating a Development Environment Creating a Development Environment
---------------------------------- ----------------------------------
First, you'll need to clone Zipline by running: First, you'll need to clone Catalyst by running:
.. code-block:: bash .. code-block:: bash
$ git clone git@github.com:your-github-username/zipline.git $ git clone git@github.com:enigmampc/catalyst.git
Then check out to a new branch where you can make your changes: Then check out to a new branch where you can make your changes:
@@ -23,15 +19,13 @@ Then check out to a new branch where you can make your changes:
$ git checkout -b some-short-descriptive-name $ git checkout -b some-short-descriptive-name
If you don't already have them, you'll need some C library dependencies. You can follow the `install guide`__ to get the appropriate dependencies. If you don't already have them, you'll need some C library dependencies. You can follow the `install guide <install.html>`_ to get the appropriate dependencies.
__ install.html
The following section assumes you already have virtualenvwrapper and pip installed on your system. Suggested installation of Python library dependencies used for development: The following section assumes you already have virtualenvwrapper and pip installed on your system. Suggested installation of Python library dependencies used for development:
.. code-block:: bash .. code-block:: bash
$ mkvirtualenv zipline $ mkvirtualenv catalyst
$ ./etc/ordered_pip.sh ./etc/requirements.txt $ ./etc/ordered_pip.sh ./etc/requirements.txt
$ pip install -r ./etc/requirements_dev.txt $ pip install -r ./etc/requirements_dev.txt
$ pip install -r ./etc/requirements_blaze.txt $ pip install -r ./etc/requirements_blaze.txt
@@ -42,104 +36,39 @@ Finally, you can build the C extensions by running:
$ python setup.py build_ext --inplace $ python setup.py build_ext --inplace
To finish, make sure `tests`__ pass. .. To finish, make sure `tests`__ pass.
__ #style-guide-running-tests .. __ #style-guide-running-tests
If you get an error running nosetests after setting up a fresh virtualenv, please try running .. If you get an error running nosetests after setting up a fresh virtualenv, please try running
.. code-block:: bash .. code-block
# where zipline is the name of your virtualenv .. # where zipline is the name of your virtualenv
$ deactivate zipline .. $ deactivate zipline
$ workon zipline .. $ workon zipline
Development with Docker .. Development with Docker
.. -----------------------
..If you want to work with zipline using a `Docker`__ container, you'll need to build the ``Dockerfile`` in the Zipline root directory, and then build ``Dockerfile-dev``. Instructions for building both containers can be found in ``Dockerfile`` and ``Dockerfile-dev``, respectively.
.. __ https://docs.docker.com/get-started/
Git Branching Structure
----------------------- -----------------------
If you want to work with zipline using a `Docker`__ container, you'll need to build the ``Dockerfile`` in the Zipline root directory, and then build ``Dockerfile-dev``. Instructions for building both containers can be found in ``Dockerfile`` and ``Dockerfile-dev``, respectively. If you want to contribute to the codebase of Catalyst, familiarize yourself with our branching structure, a fairly standardized one for that matter, that follows what is documented in the following article: `A successful Git branching model <http://nvie.com/posts/a-successful-git-branching-model/>`_. To contribute, create your local branch and submit a Pull Request (PR) to the **develop** branch.
__ https://docs.docker.com/get-started/ .. image:: https://camo.githubusercontent.com/9bde6fb64a9542a572e0e2017cbb58d9d2c440ac/687474703a2f2f6e7669652e636f6d2f696d672f6769742d6d6f64656c4032782e706e67
Style Guide & Running Tests
---------------------------
We use `flake8`__ for checking style requirements and `nosetests`__ to run Zipline tests. Our `continuous integration`__ tools will run these commands.
__ http://flake8.pycqa.org/en/latest/
__ http://nose.readthedocs.io/en/latest/
__ https://en.wikipedia.org/wiki/Continuous_integration
Before submitting patches or pull requests, please ensure that your changes pass when running:
.. code-block:: bash
$ flake8 zipline tests
In order to run tests locally, you'll need `TA-lib`__, which you can install on Linux by running:
__ https://mrjbq7.github.io/ta-lib/install.html
.. code-block:: bash
$ wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
$ tar -xvzf ta-lib-0.4.0-src.tar.gz
$ cd ta-lib/
$ ./configure --prefix=/usr
$ make
$ sudo make install
And for ``TA-lib`` on OS X you can just run:
.. code-block:: bash
$ brew install ta-lib
Then run ``pip install`` TA-lib:
.. code-block:: bash
$ pip install -r ./etc/requirements_talib.txt
You should now be free to run tests:
.. code-block:: bash
$ nosetests
Continuous Integration
----------------------
We use `Travis CI`__ for Linux-64 bit builds and `AppVeyor`__ for Windows-64 bit builds.
.. note::
We do not currently have CI for OSX-64 bit builds. 32-bit builds may work but are not included in our integration tests.
__ https://travis-ci.org/quantopian/zipline
__ https://ci.appveyor.com/project/quantopian/zipline
Packaging
---------
To learn about how we build Zipline conda packages, you can read `this`__ section in our release process notes.
__ release-process.html#uploading-conda-packages
Contributing to the Docs Contributing to the Docs
------------------------ ------------------------
If you'd like to contribute to the documentation on zipline.io, you can navigate to ``docs/source/`` where each `reStructuredText`__ (``.rst``) file is a separate section there. To add a section, create a new file called ``some-descriptive-name.rst`` and add ``some-descriptive-name`` to ``appendix.rst``. To edit a section, simply open up one of the existing files, make your changes, and save them. If you'd like to contribute to the documentation on enigmampc.github.io, you can navigate to ``docs/source/`` where each `reStructuredText <https://en.wikipedia.org/wiki/ReStructuredText>`_ file is a separate section there. To add a section, create a new file called ``some-descriptive-name.rst`` and add ``some-descriptive-name`` to ``index.rst``. To edit a section, simply open up one of the existing files, make your changes, and save them.
__ https://en.wikipedia.org/wiki/ReStructuredText
We use `Sphinx`__ to generate documentation for Zipline, which you will need to install by running:
__ http://www.sphinx-doc.org/en/stable/
We use `Sphinx <http://www.sphinx-doc.org/en/stable/>`_ to generate documentation for Catalyst, which you will need to install by running:
.. code-block:: bash .. code-block:: bash
@@ -149,7 +78,7 @@ To build and view the docs locally, run:
.. code-block:: bash .. code-block:: bash
# assuming you're in the Zipline root directory # assuming you're in the Catalyst root directory
$ cd docs $ cd docs
$ make html $ make html
$ {BROWSER} build/html/index.html $ {BROWSER} build/html/index.html
@@ -162,7 +91,7 @@ Standard prefixes to start a commit message:
.. code-block:: text .. code-block:: text
BLD: change related to building Zipline BLD: change related to building Catalyst
BUG: bug fix BUG: bug fix
DEP: deprecate something, or remove a deprecated object DEP: deprecate something, or remove a deprecated object
DEV: development tool or utility DEV: development tool or utility
@@ -172,15 +101,13 @@ Standard prefixes to start a commit message:
REV: revert an earlier commit REV: revert an earlier commit
STY: style fix (whitespace, PEP8, flake8, etc) STY: style fix (whitespace, PEP8, flake8, etc)
TST: addition or modification of tests TST: addition or modification of tests
REL: related to releasing Zipline REL: related to releasing Catalyst
PERF: performance enhancements PERF: performance enhancements
Some commit style guidelines: Some commit style guidelines:
Commit lines should be no longer than `72 characters`__. The first line of the commit should include one of the above prefixes. There should be an empty line between the commit subject and the body of the commit. In general, the message should be in the imperative tense. Best practice is to include not only what the change is, but why the change was made. Commit lines should be no longer than `72 characters <https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project>`_. The first line of the commit should include one of the above prefixes. There should be an empty line between the commit subject and the body of the commit. In general, the message should be in the imperative tense. Best practice is to include not only what the change is, but why the change was made.
__ https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project
**Example:** **Example:**
@@ -203,8 +130,6 @@ __ https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project
Formatting Docstrings Formatting Docstrings
--------------------- ---------------------
When adding or editing docstrings for classes, functions, etc, we use `numpy`__ as the canonical reference. When adding or editing docstrings for classes, functions, etc, we use `numpy <https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt>`_ as the canonical reference.
__ https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt
+444
View File
@@ -0,0 +1,444 @@
|
Example Algorithms
==================
This section documents a small number of example algorithms to complement the
beginner tutorial, and show how other trading algorithms can be implemented
using Catalyst:
.. _buy_and_hodl:
Buy and Hodl Algorithm
~~~~~~~~~~~~~~~~~~~~~~
source: `examples/buy_and_hodl.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_and_hodl.py>`_
First ingest the historical pricing data needed to run this algorithm:
.. code-block:: bash
catalyst ingest-exchange -x poloniex -f daily -i btc_usdt
Then, you can run the code below with the following command:
.. code-block:: bash
catalyst run -f buy_and_hodl.py --start 2015-3-1 --end 2017-10-31 --capital-base 100000 -x poloniex -c btc -o bah.pickle
This command will run the trading algorithm in the specified time range and
plot the resulting performance using the matplotlib library. You can choose any
date interval with the ``--start`` and ``--end`` parameters, but bear in mind
that 2015-3-1 is the earliest date that Catalyst supports (if you choose an
earlier date, you'll get an error), and the most recent date you can choose is
one day prior to the current date.
.. code-block:: python
#!/usr/bin/env python
#
# Copyright 2017 Enigma MPC, Inc.
# Copyright 2015 Quantopian, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pandas as pd
from catalyst.api import (
order_target_value,
symbol,
record,
cancel_order,
get_open_orders,
)
def initialize(context):
context.ASSET_NAME = 'btc_usdt'
context.TARGET_HODL_RATIO = 0.8
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
context.is_buying = True
context.asset = symbol(context.ASSET_NAME)
context.i = 0
def handle_data(context, data):
context.i += 1
starting_cash = context.portfolio.starting_cash
target_hodl_value = context.TARGET_HODL_RATIO * starting_cash
reserve_value = context.RESERVE_RATIO * starting_cash
# Cancel any outstanding orders
orders = get_open_orders(context.asset) or []
for order in orders:
cancel_order(order)
# Stop buying after passing the reserve threshold
cash = context.portfolio.cash
if cash <= reserve_value:
context.is_buying = False
# Retrieve current asset price from pricing data
price = data.current(context.asset, 'price')
# Check if still buying and could (approximately) afford another purchase
if context.is_buying and cash > price:
# Place order to make position in asset equal to target_hodl_value
order_target_value(
context.asset,
target_hodl_value,
limit_price=price * 1.1,
stop_price=price * 0.9,
)
record(
price=price,
volume=data.current(context.asset, 'volume'),
cash=cash,
starting_cash=context.portfolio.starting_cash,
leverage=context.account.leverage,
)
def analyze(context=None, results=None):
import matplotlib.pyplot as plt
# Plot the portfolio and asset data.
ax1 = plt.subplot(611)
results[['portfolio_value']].plot(ax=ax1)
ax1.set_ylabel('Portfolio Value (USD)')
ax2 = plt.subplot(612, sharex=ax1)
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
results[['price']].plot(ax=ax2)
trans = results.ix[[t != [] for t in results.transactions]]
buys = trans.ix[
[t[0]['amount'] > 0 for t in trans.transactions]
]
ax2.plot(
buys.index,
results.price[buys.index],
'^',
markersize=10,
color='g',
)
ax3 = plt.subplot(613, sharex=ax1)
results[['leverage', 'alpha', 'beta']].plot(ax=ax3)
ax3.set_ylabel('Leverage ')
ax4 = plt.subplot(614, sharex=ax1)
results[['starting_cash', 'cash']].plot(ax=ax4)
ax4.set_ylabel('Cash (USD)')
results[[
'treasury',
'algorithm',
'benchmark',
]] = results[[
'treasury_period_return',
'algorithm_period_return',
'benchmark_period_return',
]]
ax5 = plt.subplot(615, sharex=ax1)
results[[
'treasury',
'algorithm',
'benchmark',
]].plot(ax=ax5)
ax5.set_ylabel('Percent Change')
ax6 = plt.subplot(616, sharex=ax1)
results[['volume']].plot(ax=ax6)
ax6.set_ylabel('Volume (mCoins/5min)')
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
.. _mean_reversion:
Mean Reversion Algorithm
~~~~~~~~~~~~~~~~~~~~~~~~
source: `examples/mean_reversion_simple.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/mean_reversion_simple.py>`_
This algorithm is based on a simple momentum strategy. When the cryptoasset goes
up quickly, we're going to buy; when it goes down quickly, we're going to sell.
Hopefully, we'll ride the waves.
We are choosing to run this trading algorithm with the ``neo_usd`` currency pair
on the ``Bitfinex`` exchange. Thus, first ingest the historical pricing data
that we need, with minute resolution:
.. code-block:: bash
catalyst ingest-exchange -x bitfinex -f minute -i neo_usd
To run this algorithm, we are opting for the Python interpreter, instead of the
command line (CLI). All of the parameters for the simulation are specified in
lines 218-245, so in order to run the algorithm we just type:
.. code-block:: bash
python mean_reversion_simple.py
.. code-block:: python
import pandas as pd
import talib
from logbook import Logger
from catalyst import run_algorithm
from catalyst.api import symbol, record, order_target_percent, get_open_orders
from catalyst.exchange.stats_utils import extract_transactions
# We give a name to the algorithm which Catalyst will use to persist its state.
# In this example, Catalyst will create the `.catalyst/data/live_algos`
# directory. If we stop and start the algorithm, Catalyst will resume its
# state using the files included in the folder.
NAMESPACE = 'mean_reversion_simple'
log = Logger(NAMESPACE)
# To run an algorithm in Catalyst, you need two functions: initialize and
# handle_data.
def initialize(context):
# This initialize function sets any data or variables that you'll use in
# your algorithm. For instance, you'll want to define the trading pair (or
# trading pairs) you want to backtest. You'll also want to define any
# parameters or values you're going to use.
# In our example, we're looking at Ether in USD Tether.
context.neo_usd = symbol('neo_usd')
context.base_price = None
context.current_day = None
def handle_data(context, data):
# This handle_data function is where the real work is done. Our data is
# minute-level tick data, and each minute is called a frame. This function
# runs on each frame of the data.
# We flag the first period of each day.
# Since cryptocurrencies trade 24/7 the `before_trading_starts` handle
# would only execute once. This method works with minute and daily
# frequencies.
today = data.current_dt.floor('1D')
if today != context.current_day:
context.traded_today = False
context.current_day = today
# We're computing the volume-weighted-average-price of the security
# defined above, in the context.neo_usd variable. For this example, we're
# using three bars on the 15 min bars.
# The frequency attribute determine the bar size. We use this convention
# for the frequency alias:
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
prices = data.history(
context.neo_usd,
fields='close',
bar_count=50,
frequency='15T'
)
# Ta-lib calculates various technical indicator based on price and
# volume arrays.
# In this example, we are comp
rsi = talib.RSI(prices.values, timeperiod=14)
# We need a variable for the current price of the security to compare to
# the average. Since we are requesting two fields, data.current()
# returns a DataFrame with
current = data.current(context.neo_usd, fields=['close', 'volume'])
price = current['close']
# If base_price is not set, we use the current value. This is the
# price at the first bar which we reference to calculate price_change.
if context.base_price is None:
context.base_price = price
price_change = (price - context.base_price) / context.base_price
cash = context.portfolio.cash
# Now that we've collected all current data for this frame, we use
# the record() method to save it. This data will be available as
# a parameter of the analyze() function for further analysis.
record(
price=price,
volume=current['volume'],
price_change=price_change,
rsi=rsi[-1],
cash=cash
)
# We are trying to avoid over-trading by limiting our trades to
# one per day.
if context.traded_today:
return
# Since we are using limit orders, some orders may not execute immediately
# we wait until all orders are executed before considering more trades.
orders = get_open_orders(context.neo_usd)
if len(orders) > 0:
return
# Exit if we cannot trade
if not data.can_trade(context.neo_usd):
return
# Another powerful built-in feature of the Catalyst backtester is the
# portfolio object. The portfolio object tracks your positions, cash,
# cost basis of specific holdings, and more. In this line, we calculate
# how long or short our position is at this minute.
pos_amount = context.portfolio.positions[context.neo_usd].amount
if rsi[-1] <= 30 and pos_amount == 0:
log.info(
'{}: buying - price: {}, rsi: {}'.format(
data.current_dt, price, rsi[-1]
)
)
order_target_percent(context.neo_usd, 1)
context.traded_today = True
elif rsi[-1] >= 80 and pos_amount > 0:
log.info(
'{}: selling - price: {}, rsi: {}'.format(
data.current_dt, price, rsi[-1]
)
)
order_target_percent(context.neo_usd, 0)
context.traded_today = True
def analyze(context=None, perf=None):
import matplotlib.pyplot as plt
# The base currency of the algo exchange
base_currency = context.exchanges.values()[0].base_currency.upper()
# Plot the portfolio value over time.
ax1 = plt.subplot(611)
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
ax1.set_ylabel('Portfolio Value ({})'.format(base_currency))
# Plot the price increase or decrease over time.
ax2 = plt.subplot(612, sharex=ax1)
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
ax2.set_ylabel('{asset} ({base})'.format(
asset=context.neo_usd.symbol, base=base_currency
))
transaction_df = extract_transactions(perf)
if not transaction_df.empty:
buy_df = transaction_df[transaction_df['amount'] > 0]
sell_df = transaction_df[transaction_df['amount'] < 0]
ax2.scatter(
buy_df.index.to_pydatetime(),
perf.loc[buy_df.index, 'price'],
marker='^',
s=100,
c='green',
label=''
)
ax2.scatter(
sell_df.index.to_pydatetime(),
perf.loc[sell_df.index, 'price'],
marker='v',
s=100,
c='red',
label=''
)
ax4 = plt.subplot(613, sharex=ax1)
perf.loc[:, 'cash'].plot(
ax=ax4, label='Base Currency ({})'.format(base_currency)
)
ax4.set_ylabel('Cash ({})'.format(base_currency))
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
ax5 = plt.subplot(614, sharex=ax1)
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
ax5.set_ylabel('Percent Change')
ax6 = plt.subplot(615, sharex=ax1)
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
ax6.axhline(70, color='darkgoldenrod')
ax6.axhline(30, color='darkgoldenrod')
if not transaction_df.empty:
ax6.scatter(
buy_df.index.to_pydatetime(),
perf.loc[buy_df.index, 'rsi'],
marker='^',
s=100,
c='green',
label=''
)
ax6.scatter(
sell_df.index.to_pydatetime(),
perf.loc[sell_df.index, 'rsi'],
marker='v',
s=100,
c='red',
label=''
)
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
pass
if __name__ == '__main__':
# The execution mode: backtest or live
MODE = 'backtest'
if MODE == 'backtest':
# catalyst run -f catalyst/examples/mean_reversion_simple.py -x poloniex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000
run_algorithm(
capital_base=10000,
data_frequency='minute',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
algo_namespace=NAMESPACE,
base_currency='usd',
start=pd.to_datetime('2017-10-1', utc=True),
end=pd.to_datetime('2017-11-10', utc=True),
)
elif MODE == 'live':
run_algorithm(
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
live=True,
algo_namespace=NAMESPACE,
base_currency='usd',
live_graph=True
)
+17 -4
View File
@@ -1,12 +1,25 @@
.. include:: ../../README.rst .. include:: welcome.rst
|
|
Table of Contents
-----------------
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
install install
beginner-tutorial beginner-tutorial
bundles jupyter
live-trading
naming-convention
example-algos
utilities
videos
resources
development-guidelines development-guidelines
appendix
release-process
releases releases
.. bundles
.. development-guidelines
.. appendix
.. release-process
+399 -56
View File
@@ -1,43 +1,303 @@
Install Install
======= =======
To get started with Catalyst, you will need to install it in your computer.
Like any other piece of software, Catalyst has a number of dependencies
(other software on which it depends to run) that you will need to install, as
well. We recommend using a software named ``Conda`` that will manage all
these dependencies for you, and set up the environment needed to get you up
and running as easily as possible. This is the recommended installation method
for Windows, MacOS and Linux. See :ref:`Installing with Conda <conda>`.
What conda does is create a pre-configured environment, and inside that
environment install Catalyst using ``pip``, Python's package manager. Thus,
as an alternative installation method for MacOS and Linux, you can install
Catalyst directly with ``pip`` (we recommend in combination with a virtual
environemnt). See :ref:`Installing with pip <pip>`.
Regardless of the method, each operating system (OS), has its own
prerequisites, make sure to review the corresponding sections for your system:
:ref:`Linux <linux>`, :ref:`MacOS <macos>` and :ref:`Windows <windows>`.
.. _conda:
Installing with ``conda``
-------------------------
The preferred method to install Catalyst is via the ``conda`` package manager,
which comes as part of Continuum Analytics' `Anaconda
<http://continuum.io/downloads>`_ distribution.
The primary advantage of using Conda over ``pip`` is that conda natively
understands the complex binary dependencies of packages like ``numpy`` and
``scipy``. This means that ``conda`` can install Catalyst and its
dependencies without requiring the use of a second tool to acquire Catalyst's
non-Python dependencies.
For Windows, you will first need to install the *Microsoft Visual C++
Compiler for Python 2.7*. Follow the instructions on the :ref:`Windows
<windows>` section and come back here.
For instructions on how to install ``conda``, see the `Conda Installation
Documentation <http://conda.pydata.org/docs/download.html>`_. Alternatively,
you can install MiniConda, which is a smaller footprint (fewer packages and
smaller size) than its big brother Anaconda, but it still contains all the
main packages needed. To install MiniConda, you can follow these steps:
1. Download `MiniConda <https://conda.io/miniconda.html>`_. Select Python 2.7
for your Operating System.
2. Install MiniConda. See the `Installation Instructions
<https://conda.io/docs/user-guide/install/index.html>`_ if you need help.
3. Ensure the correct installation by running ``conda list`` in a Terminal
window, which should print the list of packages installed with Conda.
For Windows, if you accepted the default installation options, you didn't
check an option to add Conda to the PATH, so trying to run ``conda`` from
a regular ``Command Prompt`` will result in the following error: ``'conda'
is no recognized as an internal or external command, operatble program or
batch file``. That's to be expected. You will nee to launch an ``Anaconda
Prompt`` that was added at installation time to your list of programs
available from the Start menu.
Once either Conda or MiniConda has been set up you can install Catalyst:
1. Download the file `python2.7-environment.yml
<https://github.com/enigmampc/catalyst/blob/master/etc/python2.7-environment.yml>`_.
To download, simply click on the 'Raw' button and save the file locally
to a folder you can remember. Make sure that the file gets saved with the
``.yml`` extension, and nothing like a ``.txt`` file or anything else.
2. Open a Terminal window and enter [``cd/dir``] into the directory where you
saved the above ``python2.7-environment.yml`` file.
3. Install using this file. This step can take about 5-10 minutes to install.
.. code-block:: bash
conda env create -f python2.7-environment.yml
4. Activate the environment (which you need to do every time you start a new
session to run Catalyst):
**Linux or MacOS:**
.. code-block:: bash
source activate catalyst
**Windows:**
.. code-block:: bash
activate catalyst
5. Verify that Catalyst is install correctly:
.. code-block:: bash
catalyst --version
which should display the current version.
Congratulations! You now have Catalyst installed.
Troubleshooting ``conda`` Install
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the command ``conda env create -f python2.7-environment.yml`` in step 3
above failed for any reason, you can try setting up the environment manually
with the following steps:
1. If the above installation failed, and you have a partially set up catalyst
environment, remove it first. If you are starting from scratch, proceed to
step #2:
.. code-block:: bash
conda env remove --name catalyst
2. Create the environment:
.. code-block:: bash
conda create --name catalyst python=2.7 scipy zlib
3. Activate the environment:
**Linux or MacOS:**
.. code-block:: bash
source activate catalyst
**Windows:**
.. code-block:: bash
activate catalyst
4. Install the Catalyst inside the environment:
.. code-block:: bash
pip install enigma-catalyst matplotlib
5. Verify that Catalyst is installed correctly:
.. code-block:: bash
catalyst --version
which should display the current version.
Congratulations! You now have Catalyst properly installed.
.. _pip:
Installing with ``pip`` Installing with ``pip``
----------------------- -----------------------
Installing Zipline via ``pip`` is slightly more involved than the average Installing Catalyst via ``pip`` is slightly more involved than the average
Python package. Python package.
There are two reasons for the additional complexity: There are two reasons for the additional complexity:
1. Zipline ships several C extensions that require access to the CPython C API. 1. Catalyst ships several C extensions that require access to the CPython C
In order to build the C extensions, ``pip`` needs access to the CPython API. In order to build the C extensions, ``pip`` needs access to the
header files for your Python installation. CPython header files for your Python installation.
2. Zipline depends on `numpy <http://www.numpy.org/>`_, the core library for 2. Catalyst depends on `numpy <http://www.numpy.org/>`_, the core library for
numerical array computing in Python. Numpy depends on having the `LAPACK numerical array computing in Python. Numpy depends on having the `LAPACK
<http://www.netlib.org/lapack>`_ linear algebra routines available. <http://www.netlib.org/lapack>`_ linear algebra routines available.
Because LAPACK and the CPython headers are non-Python dependencies, the correct Because LAPACK and the CPython headers are non-Python dependencies, the
way to install them varies from platform to platform. If you'd rather use a correctway to install them varies from platform to platform. If you'd rather
single tool to install Python and non-Python dependencies, or if you're already use a single tool to install Python and non-Python dependencies, or if you're
using `Anaconda <http://continuum.io/downloads>`_ as your Python distribution, already using `Anaconda <http://continuum.io/downloads>`_ as your Python
you can skip to the :ref:`Installing with Conda <conda>` section. distribution, refer to the :ref:`Installing with Conda <conda>` section.
Once you've installed the necessary additional dependencies (see below for Once you've installed the necessary additional dependencies for your system
your particular platform), you should be able to simply run (see below for your particular platform: :ref:`Linux`, :ref:`MacOS` or
:ref:`Windows`), you should be able to simply run
.. code-block:: bash .. code-block:: bash
$ pip install zipline $ pip install enigma-catalyst matplotlib
If you use Python for anything other than Zipline, we **strongly** recommend Note that in the command above we install two different packages. The second
one, ``matplotlib`` is a visualization library. While it's not strictly
required to run catalyst simulations or live trading, it comes in very handy
to visualize the performance of your algorithms, and for this reason we
recommend you install it, as well.
If you use Python for anything other than Catalyst, we **strongly** recommend
that you install in a `virtualenv that you install in a `virtualenv
<https://virtualenv.readthedocs.org/en/latest>`_. The `Hitchhiker's Guide to <https://virtualenv.readthedocs.org/en/latest>`_. The `Hitchhiker's Guide to
Python`_ provides an `excellent tutorial on virtualenv Python`_ provides an `excellent tutorial on virtualenv
<http://docs.python-guide.org/en/latest/dev/virtualenvs/>`_. <http://docs.python-guide.org/en/latest/dev/virtualenvs/>`_. Here's a
summarized version:
GNU/Linux .. code-block:: bash
~~~~~~~~~
$ pip install virtualenv
$ virtualenv catalyst-venv
$ source ./catalyst-venv/bin/activate
$ pip install enigma-catalyst matplotlib
Troubleshooting ``pip`` Install
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Issue**:
Package enigma-catalyst cannot be found
**Solution**:
Make sure you have the most up-to-date version of pip installed, by running:
.. code-block:: bash
pip install --upgrade pip
On Windows, the recommended command is:
.. code-block:: bash
python -m pip install --upgrade pip
----
**Issue**:
Package enigma-catalyst cannot still be found, even after upgrading pip
(see above), with an error similar to:
.. code-block:: bash
Downloading/unpacking enigma-catalyst
Could not find a version that satisfies the requirement enigma-catalyst
(from versions: 0.1.dev9, 0.2.dev2, 0.1.dev4, 0.1.dev5, 0.1.dev3,
0.2.dev1, 0.1.dev8, 0.1.dev6)
Cleaning up...
No distributions matching the version for enigma-catalyst
**Solution**:
In some systems (this error has been reported in Ubuntu), pip is configured
to only find stable versions by default. Since Catalyst is in alpha
version, pip cannot find a matching version that satisfies the installation
requirements. The solution is to include the `--pre` flag to include
pre-release and development versions:
.. code-block:: bash
pip install --pre enigma-catalyst
----
**Issue**:
Package enigma-catalyst fails to install because of outdated setuptools
**Solution**:
Upgrade to the most up-to-date setuptools package by running:
.. code-block:: bash
pip install --upgrade pip setuptools
----
**Issue**:
Missing required packages
**Solution**:
Download `requirements.txt
<https://github.com/enigmampc/catalyst/blob/master/etc/requirements.txt>`_
(click on the *Raw* button and Right click -> Save As...) and use it to
install all the required dependencies by running:
.. code-block:: bash
pip install -r requirements.txt
----
**Issue**:
Installation fails with error:
``fatal error: Python.h: No such file or directory``
**Solution**:
Some systems (this issue has been reported in Ubuntu) require `python-dev`
for the proper build and installation of package dependencies. The solution
is to install python-dev, which is independent of the virtual environment.
In Ubuntu, you would need to run:
.. code-block:: bash
sudo apt-get install python-dev
.. _linux:
GNU/Linux Requirements
----------------------
On `Debian-derived`_ Linux distributions, you can acquire all the necessary On `Debian-derived`_ Linux distributions, you can acquire all the necessary
binary dependencies from ``apt`` by running: binary dependencies from ``apt`` by running:
@@ -60,25 +320,50 @@ On `Arch Linux`_, you can acquire the additional dependencies via ``pacman``:
$ pacman -S lapack gcc gcc-fortran pkg-config $ pacman -S lapack gcc gcc-fortran pkg-config
There are also AUR packages available for installing `Python 3.4 .. Commenting it out until Catalyst fully supports Python 3.X
<https://aur.archlinux.org/packages/python34/>`_ (Arch's default python is now ..
3.5, but Zipline only currently supports 3.4), and `ta-lib .. There are also AUR packages available for installing `Python 3.4
<https://aur.archlinux.org/packages/ta-lib/>`_, an optional Zipline dependency. .. <https://aur.archlinux.org/packages/python34/>`_ (Arch's default python is now
Python 2 is also installable via: .. 3.5, but Catalyst only currently supports 3.4), and `ta-lib
.. <https://aur.archlinux.org/packages/ta-lib/>`_, an optional Catalyst dependency.
.. Python 2 is also installable via:
..
.. $ pacman -S python2
Amazon Linux AMI Notes
~~~~~~~~~~~~~~~~~~~~~~
The packages ``pip`` and ``setuptools`` that come shipped by default are very
outdated. Thus, you first need to run:
.. code-block:: bash .. code-block:: bash
$ pacman -S python2 pip install --upgrade pip setuptools
OSX The default installation is also missing the C and C++ compilers, which you
~~~ install by:
The version of Python shipped with OSX by default is generally out of date, and .. code-block:: bash
has a number of quirks because it's used directly by the operating system. For
these reasons, many developers choose to install and use a separate Python sudo yum install gcc gcc-c++
Then you should follow the regular installation instructions outlined at the
beginning of this page.
.. _MacOS:
MacOS Requirements
------------------
The version of Python shipped with MacOS by default is generally out of date,
and has a number of quirks because it's used directly by the operating system.
For these reasons, many developers choose to install and use a separate Python
installation. The `Hitchhiker's Guide to Python`_ provides an excellent guide installation. The `Hitchhiker's Guide to Python`_ provides an excellent guide
to `Installing Python on OSX <http://docs.python-guide.org/en/latest/>`_, which to `Installing Python on MacOS <http://docs.python-guide.org/en/latest/>`_,
explains how to install Python with the `Homebrew`_ manager. which explains how to install Python with the `Homebrew`_ manager.
Assuming you've installed Python with Homebrew, you'll also likely need the Assuming you've installed Python with Homebrew, you'll also likely need the
following brew packages: following brew packages:
@@ -87,36 +372,94 @@ following brew packages:
$ brew install freetype pkg-config gcc openssl $ brew install freetype pkg-config gcc openssl
Windows MacOS + virtualenv + matplotlib
~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For windows, the easiest and best supported way to install zipline is to use A note about using matplotlib in virtual enviroments on MacOS: it may be
:ref:`Conda <conda>`. necessary to run
.. _conda:
Installing with ``conda``
-------------------------
Another way to install Zipline is via the ``conda`` package manager, which
comes as part of Continuum Analytics' `Anaconda
<http://continuum.io/downloads>`_ distribution.
The primary advantage of using Conda over ``pip`` is that conda natively
understands the complex binary dependencies of packages like ``numpy`` and
``scipy``. This means that ``conda`` can install Zipline and its dependencies
without requiring the use of a second tool to acquire Zipline's non-Python
dependencies.
For instructions on how to install ``conda``, see the `Conda Installation
Documentation <http://conda.pydata.org/docs/download.html>`_
Once conda has been set up you can install Zipline from our ``Quantopian``
channel:
.. code-block:: bash .. code-block:: bash
conda install -c Quantopian zipline echo "backend: TkAgg" > ~/.matplotlib/matplotlibrc
in order to override the default ``MacOS`` backend for your system, which
may not be accessible from inside the virtual environment. This will allow
Catalyst to open matplotlib charts from within a virtual environment, which
is useful for displaying the performance of your backtests. To learn more
about matplotlib backends, please refer to the
`matplotlib backend documentation <https://matplotlib.org/faq/usage_faq.html#what-is-a-backend>`_.
.. _windows:
Windows Requirements
--------------------
In Windows, you will first need to install the `Microsoft Visual C++ Compiler
for Python 2.7
<https://www.microsoft.com/en-us/download/details.aspx?id=44266>`_. This
package contains the compiler and the set of system headers necessary for
producing binary wheels for Python 2.7 packages. If it's not already in your
system, download it and install it before proceeding to the next step.
Once you have the above compiler installed, the easiest and best supported way
to install Catalyst in Windows is to use :ref:`Conda <conda>`. If you didn't
any problems installing the compiler, jump to the :ref:`Conda <conda>` section,
otherwise keep on reading to troubleshoot the C++ compiler installtion.
Some problems we have encountered installing the **Visual C++ Compiler**
mentioned above are as follows:
- **The system administrator has set policies to prevent this installation**.
In some systems, there is a default *Windows Software Restriction* policy
that prevents the installation of some software packages like this one.
You'll have to change the Registry to circumvent this:
- Click ``Start``, and search for ``regedit`` and launch the
``Registry Editor``
- Navigate to the following folder:
``HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Installer``
- If the last folder does not exist, create it by right-clicking on the
parent folder and choosing -> ``New`` -> ``Key`` and typing ``Installer``
- If there is an entry for ``DisableMSI``, set the Value data to 0.
- If there is no such entry, click on the ``Edit`` menu -> ``New`` ->
``DWORD (32-bit) Value`` and enter ``DisableMSI`` as the Name (and by
default you get 0 as the Value Data)
|
- **The installer has encountered an unexpected error installing this package.
This may indicate a problem with this package. The error code is 2503.**
We have observed this when trying to install a package without enough
administrator permissions. Even when you are logged in as an Administrator,
you have to explictily install this package with administrator privileges:
- Click ``Start`` and find ``CMD`` or ``Command Prompt``
- Right click on it and choose ``Run as administrator``
- ``cd`` into the folder where you downloaded ``VCForPython27.msi``
- Run ``msiexec /i VCForPython27.msi``
Getting Help
------------
If after following the instructions above, and going through the
*Troubleshooting* sections, you still experience problems installing Catalyst,
you can seek additional help through the following channels:
- Join our `Discord community <https://discord.gg/SJK32GY>`_, and head over
the #catalyst_dev channel where many other users (as well as the project
developers) hang out, and can assist you with your particular issue. The
more descriptive and the more information you can provide, the easiest will
be for others to help you out.
- Report the problem you are experiencing on our
`GitHub repository <https://github.com/enigmampc/catalyst/issues>`_
following the guidelines provided therein. Before you do so, take a moment
to browse through all `previous reported issues
<https://github.com/enigmampc/catalyst/issues?utf8=%E2%9C%93&q=is%3Aissue>`_
in the likely case that someone else experienced that same issue before,
and you get a hint on how to solve it.
.. _`Debian-derived`: https://www.debian.org/misc/children-distros .. _`Debian-derived`: https://www.debian.org/misc/children-distros
.. _`RHEL-derived`: https://en.wikipedia.org/wiki/Red_Hat_Enterprise_Linux_derivatives .. _`RHEL-derived`: https://en.wikipedia.org/wiki/Red_Hat_Enterprise_Linux_derivatives
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
Live Trading
============
This document explains how to get started with live trading.
Supported Exchanges
^^^^^^^^^^^^^^^^^^^
Catalyst can trade against these exchanges:
- Bitfinex, id= ``bitfinex``
- Bittrex, id= ``bittrex``
- Poloniex, id= ``poloniex``
Authentication
^^^^^^^^^^^^^^
Most exchanges require token key/secret combination for authentication. By
convention, Catalyst uses an ``auth.json`` file to hold this data.
This example illustrates the convention using the *Bitfinex* exchange.
Here is how to generate key and secret values for the Bitfinex exchange:
https://docs.bitfinex.com/v1/docs/api-access. Most exchanges follow
a similar process.
The auth.json file:
.. code-block:: json
{
"name": "bitfinex",
"key": "my-key",
"secret": "my-secret"
}
The file goes here: ``~/.catalyst/data/exchanges/bitfinex/auth.json``
Note that the `bitfinex` part in the directory above corresponds to the id of the Bitfinex
exchange as defined in the "Supported Exchanges" section above.
Attempting to run an algorithm where the targeted exchange is missing
its ``auth.json`` file will create the directory structure and create an empty
auth.json file, but will result in an error.
Currency Symbols
^^^^^^^^^^^^^^^^
Catalyst introduces a universal convention to reference
trading pairs and individual currencies. This
is required to ensure that the ``symbol()`` api predictably
returns the correct asset regardless of the targeted exchange.
Exchanges tend to use their own convention to represent currencies
(e.g. XBT and BTC both represent Bitcoin on different exchanges).
Trading pairs are also inconsistent. For example, Bitfinex
puts the market currency before the base currency without a
separator, Bittrex puts the base currency first and uses a dash
seperator.
Here is the Catalyst convention:
*[Market Currency]_[Base Currency]* all lowercase.
Currency symbols (e.g. btc, eth, ltc) follow the Bittrex convention.
Here are some examples:
.. code-block:: json
# With Bitfinex
bitcoin_usd_asset = symbol('btc_usd')
ethereum_bitcoin_asset = symbol('eth_btc')
# With Bittrex
ethereum_bitcoin_asset = symbol('eth_btc')
neo_ethereum_asset = symbol('neo_eth)
Note that the trading pairs are always referenced in the same manner.
However, not all trading pairs are available on all exchanges. An
error will occur if the specified trading pair is not trading
on the exchange. To check which currency pairs are available on each
of the supported exchanges, see `Catalyst Market Coverage <https://www.enigma.co/catalyst/status`_.
Trading an Algorithm
^^^^^^^^^^^^^^^^^^^^
There is no special convention to follow when writing an
algorithm for live trading. The same algorithm should work in
backtest and live execution mode without modification.
What differs are the arguments provided to the catalyst client or
`run_algorithm()` interface. Here is the same example in both interfaces:
.. code-block:: bash
catalyst live -f my_algo_code -x bitfinex -c btc -n my_algo_name
.. code-block:: python
run_algorithm(
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
live=True,
algo_namespace='my_algo_name',
base_currency='btc'
)
Here is the breakdown of the new arguments:
- ``live``: Boolean flag which enables live trading.
- ``exchange_name``: The name of the targeted exchange
(supported values: *bitfinex*, *bittrex*).
- ``algo_namespace``: A arbitrary label assigned to your algorithm for
data storage purposes.
- ``base_currency``: The base currency used to calculate the
statistics of your algorithm. Currently, the base currency of all
trading pairs of your algorithm must match this value.
Here is a complete algorithm for reference:
`Buy Low and Sell High <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_low_sell_high_live.py>`_
+66
View File
@@ -0,0 +1,66 @@
Naming Convention
=================
Catalyst introduces a standardized naming convention for all asset pairs
trading on any exchange in the following form:
**{market_currency}_{base_currency}**
Where {market_currency} is the asset to be traded using {base_currency} as
the reference, both written in lowercase and separated with an underscore.
This standardization is needed to overcome the lack of consistency in the
naming of assets across different exchanges, and making it easier to the user
to refer to the asset pairs that you want to trade.
Catalyst maintains a `Market Coverage Overview <https://www.enigma.co/catalyst/status>`_
where you can check the mapping between Catalyst naming pairs and that of each
exchange. Catalyst will always expect in all its functions that you will refer to
the asset pairs by using the Catalyst naming convention.
If at any point, you input the wrong name for an asset pair, you will get an error
of that pair not found in the given exchange, and a list of pairs available on that exchange:
.. code-block:: bash
$ catalyst ingest-exchange -x poloniex -i btc_usd
.. parsed-literal::
Ingesting exchange bundle poloniex...
Error traceback: /Volumes/Data/Users/victoris/Desktop/Enigma/user-install/catalyst-dev/catalyst/exchange/exchange.py (line 175)
SymbolNotFoundOnExchange: Symbol btc_usd not found on exchange Poloniex.
Choose from: ['rep_usdt', 'gno_btc', 'xvc_btc', 'pink_btc', 'sys_btc',
'emc2_btc', 'rads_btc', 'note_btc', 'maid_btc', 'bch_btc', 'gnt_btc',
'bcn_btc', 'rep_btc', 'bcy_btc', 'cvc_btc', 'nxt_xmr', 'zec_usdt',
'fct_btc', 'gas_btc', 'pot_btc', 'eth_usdt', 'btc_usdt', 'lbc_btc',
'dcr_btc', 'etc_usdt', 'omg_eth', 'amp_btc', 'xpm_btc', 'nxt_btc',
'vtc_btc', 'steem_eth', 'blk_xmr', 'pasc_btc', 'zec_xmr', 'grc_btc',
'nxc_btc', 'btcd_btc', 'ltc_btc', 'dash_btc', 'naut_btc', 'zec_eth',
'zec_btc', 'burst_btc', 'zrx_eth', 'bela_btc', 'steem_btc', 'etc_btc',
'eth_btc', 'huc_btc', 'strat_btc', 'lsk_btc', 'exp_btc', 'clam_btc',
'rep_eth', 'dash_xmr', 'cvc_eth', 'bch_usdt', 'zrx_btc', 'dash_usdt',
'blk_btc', 'xrp_btc', 'nxt_usdt', 'neos_btc', 'omg_btc', 'bts_btc',
'doge_btc', 'gnt_eth', 'sbd_btc', 'gno_eth', 'xcp_btc', 'ltc_usdt',
'btm_btc', 'xmr_usdt', 'lsk_eth', 'omni_btc', 'nav_btc', 'fldc_btc',
'ppc_btc', 'xbc_btc', 'dgb_btc', 'sc_btc', 'btcd_xmr', 'vrc_btc',
'ric_btc', 'str_btc', 'maid_xmr', 'xmr_btc', 'sjcx_btc', 'via_btc',
'xem_btc', 'nmc_btc', 'etc_eth', 'ltc_xmr', 'ardr_btc', 'gas_eth',
'flo_btc', 'xrp_usdt', 'game_btc', 'bch_eth', 'bcn_xmr', 'str_usdt']
In the example above, exchange Poloniex does not use USD, but uses instead the
USDT cryptocurrency asset that is issued on the Bitcoin blockchain via the Omni
Layer Protocol. Each USDT unit is backed by a U.S Dollar held in the reserves of
Tether Limited. USDT can be transferred, stored, and spent, just like bitcoins
or any other cryptocurrency. Given its 1:1 mapping to the USD, is a viable alternative.
.. code-block:: bash
$ catalyst ingest-exchange -x poloniex -i btc_usdt
.. parsed-literal::
Ingesting exchange bundle poloniex...
[====================================] Fetching poloniex daily candles: : 100%
+255 -11
View File
@@ -2,24 +2,268 @@
Release Notes Release Notes
============= =============
.. include:: whatsnew/1.1.1.txt Version 0.3.10
^^^^^^^^^^^^^
**Release Date**: 2017-11-28
.. include:: whatsnew/1.1.0.txt Bug Fixes
~~~~~~~~~
.. include:: whatsnew/1.0.2.txt - Fixed issue with fetching assets with daily frequency
.. include:: whatsnew/1.0.1.txt Version 0.3.9
^^^^^^^^^^^^^
**Release Date**: 2017-11-28
.. include:: whatsnew/1.0.0.txt Bug Fixes
~~~~~~~~~
.. include:: whatsnew/0.9.0.txt - Fixed sortino warning issues (:issue:`77`)
- Adjusted computation of last candle of data.history (:issue:`71`)
.. include:: whatsnew/0.8.4.txt Build
~~~~~
- Added capital_base parameter to live mode to limit cash (:issue:`79`)
- Added support for csv ingestion (:issue:`65`)
- Improved cash display in running stats (:issue:`80`)
.. include:: whatsnew/0.8.3.txt
.. include:: whatsnew/0.8.0.txt Version 0.3.8
^^^^^^^^^^^^^
**Release Date**: 2017-11-14
.. include:: whatsnew/0.7.0.txt Bug Fixes
~~~~~~~~~
.. include:: whatsnew/0.6.1.txt - Fixed a warning filter issue introduced with the latest release
Version 0.3.7
^^^^^^^^^^^^^
**Release Date**: 2017-11-14
Bug Fixes
~~~~~~~~~
- Fixed an SSL cert issue (:issue:`64`)
- Fixed cumulative stats warnings (:issue:`63`)
- Disabled auto-ingestion because of unresolved caching issues (:issue:`47`)
- Standardized live-trading stats (:issue:`61`)
Build
~~~~~
- Added a mean-reversion sample algo
- Added minutely stats in the analyze() function (:issue:`62`)
- Added specificity to some error messages
Version 0.3.6
^^^^^^^^^^^^^
**Release Date**: 2017-11-4
Bug Fixes
~~~~~~~~~
- Fixed an issue with single bar data.history() (:issue:`55`)
Version 0.3.5
^^^^^^^^^^^^^
**Release Date**: 2017-11-4
Bug Fixes
~~~~~~~~~
- Added workaround for: KeyError: Timestamp error (:issue:`53`)
Version 0.3.4
^^^^^^^^^^^^^
**Release Date**: 2017-11-2
Bug Fixes
~~~~~~~~~
- Fixed issue with auto-ingestion of minute data (:issue:`47`)
- Fixed issue with sell orders in backtesting
- Fixed data frequency issues with data.history() in backtesting
- Fixed an issue with can_trade()
- Reduced the commission and slippage values to account for lower volume
transactions
Build
~~~~~
- Added more unit tests
Documentation
~~~~~~~~~~~~~
- Improved installation notes for Windows C++ compiler and Conda
- Addition of
`Jupyter Notebook guide <https://enigmampc.github.io/catalyst/jupyter.html>`_
- Addition of
`Live Trading page <https://enigmampc.github.io/catalyst/live-trading.html>`_
- Addition of
`Videos page <https://enigmampc.github.io/catalyst/videos.html>`_
- Addition of
`Resources page <https://enigmampc.github.io/catalyst/resources.html>`_
- Addition of `Development Guidelines
<https://enigmampc.github.io/catalyst/development-guidelines.html>`_
- Addition of
`Release Notes <https://enigmampc.github.io/catalyst/releases.html>`_
- Updated code docstrings
Version 0.3.3
^^^^^^^^^^^^^
**Release Date**: 2017-10-26
Bug Fixes
~~~~~~~~~
- Fix missing -x in ingest-exchange
- Fix issue with daily chunks end date (data bundles)
- Fix issue in the prepare_chunk logic (data bundles)
Build
~~~~~
- Added data validation unit tests
Version 0.3.2
^^^^^^^^^^^^^
**Release Date**: 2017-10-25
Bug Fixes
~~~~~~~~~
- Fix to work with empty data bundles
- Fix Windows path of ``$HOME/.catalyst`` folder
- Fix ``etc/python2.7-environment.yml`` for Windows Conda install
- Fix hash method to create sid numbers compatible across platforms
- Fix an issue with asset date in chunks
Build
~~~~~
- Python3 adjustments
- Added method to clean bundle folders, and remove symbols.json
- Implemented and improved unit tests
Version 0.3.1
^^^^^^^^^^^^^
**Release Date**: 2017-10-22
Bug Fixes
~~~~~~~~~
- Fixed OS-dependent path issue in data bundle
- Changed handling of empty ``auth.json``, instead of throwing an error for
missing file
- Updated ``etc/python2.7-environment.yml`` to work with Catalyst version 0.3
- Updated ``catalyst/examples/buy_and_hodl.py`` and
``catalyst/examples/buy_low_sell_high.py`` to work with Catalyst version 0.3
Version 0.3
^^^^^^^^^^^
**Release Date**: 2017-10-20
- Standardized live and backtesting syntax
- Added a repository for historical data
- Added supported for multiple exchanges per algorithm
- Added a standardized dictionary of symbols for each exchange
- Added auto-ingestion of bundle data while backtesting
- Bug fixes
Version 0.2.dev5
^^^^^^^^^^^^^^^^
**Release Date**: 2017-10-03
- Fixes bug in data.history function that was formatting 'volume' data as
integers, now they are returned as floats with up to 9 decimals of precision.
Data bundles redone.
Version 0.2.dev4
^^^^^^^^^^^^^^^^
**Release Date**: 2017-09-20
- Fixes bug in the pricing resolution of 1-minute data, now set to 8 decimal
places. Pricing resolution of daily data remains set to 9 decimal places.
- The current data bundle takes 340MB compressed for download, and 460MB
uncompressed on disk for Catalyst to use.
Version 0.2.dev3
^^^^^^^^^^^^^^^^
**Release Date**: 2017-09-20
- 1-minute resolution OHLCV data bundle for backtesting from Poloniex exchange
- Implementation of trading of fractional crypto assets (i.e. 0.01 BTC)
- Minimum trade size of a coin can be configured on a per-coin basis, defaults
to 0.00000001 in backtesting (most exchanges set the minimum trade to larger
amounts, which will impact live trading)
- Increased pricing resolution from 3 to 9 decimal places
- The current data bundle takes 40MB compressed for download, and 99MB
uncompressed on disk for Catalyst to use.
Version 0.2.dev2
^^^^^^^^^^^^^^^^
**Release Date**: 2017-09-07
- Fix path issue
Version 0.2.dev1
^^^^^^^^^^^^^^^^
**Release Date**: 2017-09-03
- Implementation of live trading:
- Comprehensive trading functionality against exchanges Bitfinex and Bittrex.
- Support for all trading pairs available on each exchange.
- Multiple algorithms can trade simultaneously against a single exchange
using the same account.
- Each algorithm has a persisted state (i.e. algorithm can be stopped and
restarted preserving the state without data loss) that tracks all open
orders, executed transactions and portfolio positions.
- Minute by minute portfolio performance metrics.
- Daily summary performance statistics compatible with pyfolio, a Python
library for performance and risk analysis of financial portfolios
Version 0.1.dev9
^^^^^^^^^^^^^^^^
**Release Date**: 2017-08-28
- Retrieval of crypto benchmark from bundle, instead of hitting Poloniex
exchange directly
- Change of bundle storage provider from Dropbox to AWS
- Fix issue with 1/1000 scaling issue of prices in bundle
Version 0.1.dev8
^^^^^^^^^^^^^^^^
**Release Date**: 2017-08-18
- Fixes issue in the creation of bundles (:issue:`27`)
Version 0.1.dev7
^^^^^^^^^^^^^^^^
- Fixes issues in empty benchmark (:issue:`16`)
- Fixes issue of normalizing timestamps before comparison (:issue:`24`)
- Generic data bundles
- CLI UI improvements
Version 0.1.dev6
^^^^^^^^^^^^^^^^
**Release Date**: 2017-07-13
- Initial public release
+26
View File
@@ -0,0 +1,26 @@
Resources
=========
- `Catalyst Whitepaper <https://www.enigma.co/enigma_catalyst.pdf>`_
Related 3rd Party APIs
^^^^^^^^^^^^^^^^^^^^^^
- `Zipline <http://www.zipline.io/appendix.html>`_ is a Pythonic Algorithmic
Trading Library, and the project Catalyst forked off in the spring of 2017.
- `Quantopian <https://www.quantopian.com/help>`_ provides a platform for
freelance quantitative analysts develop, test, and use trading algorithms to
buy and sell securities. They aim to create a crowd-sourced hedge fund by
fostering their community of freelance traders. Quantopian's backtesting and
live-trading engine is powered by *Zipline*.
- `Pandas <https://pandas.pydata.org/pandas-docs/stable/api.html>`_ is a Python
library providing high-performance, easy-to-use data structures and data
analysis tools. Catalyst relies heavily on pandas, and many API functions
return data as Pandas dataframes.
- `Numpy <https://docs.scipy.org/doc/numpy/reference/>`_ is the fundamental
package for scientific computing with Python. Some of the data computation
that your algorithms will need, will be optimized leveraging Numpy.
- `Matplotlib <https://matplotlib.org/1.5.3/api/index.html>`_ is a Python 2D
plotting library that many of examples rely on to plot the performance of
trading algorithms

Some files were not shown because too many files have changed in this diff Show More