├── .github └── workflows │ └── python-app.yml ├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── crypto_data_fetcher ├── __init__.py ├── binance_future.py ├── binance_spot.py ├── bybit.py ├── ftx.py ├── gmo.py ├── kraken.py ├── okex.py └── utils.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_binance_future.py ├── test_binance_spot.py ├── test_bybit.py ├── test_bybit_linear.py ├── test_ftx.py ├── test_ftx_private.py ├── test_gmo.py ├── test_kraken.py ├── test_okex.py └── test_utils.py /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: 9 | - '**' 10 | pull_request: 11 | branches: [ master ] 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python-version: [ '3.6' ] 20 | ccxt-version: [ '1.57.43' ] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | architecture: x64 28 | - name: Run tests 29 | run: | 30 | pip install pipenv 31 | pipenv install 32 | pipenv install ccxt==${{ matrix.ccxt-version }} 33 | pipenv run python -m unittest tests/test_* 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python 2 | # Edit at https://www.gitignore.io/?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # pyenv 71 | .python-version 72 | 73 | # pipenv 74 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 75 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 76 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 77 | # install all needed dependencies. 78 | #Pipfile.lock 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | .spyproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | # Mr Developer 94 | .mr.developer.cfg 95 | .project 96 | .pydevproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | .dmypy.json 104 | dmypy.json 105 | 106 | # Pyre type checker 107 | .pyre/ 108 | 109 | # End of https://www.gitignore.io/api/python 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | ccxt = "==1.57.43" 10 | pandas = "*" 11 | six = "*" 12 | joblib = "*" 13 | 14 | [requires] 15 | python_version = "3.6" 16 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "6cbc1d62316650af278ba042d46b3767d232485b073f8b82dfc92c6767665f74" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aiodns": { 20 | "hashes": [ 21 | "sha256:2b19bc5f97e5c936638d28e665923c093d8af2bf3aa88d35c43417fa25d136a2", 22 | "sha256:946bdfabe743fceeeb093c8a010f5d1645f708a241be849e17edfb0e49e08cd6" 23 | ], 24 | "markers": "python_version >= '3.5.2'", 25 | "version": "==3.0.0" 26 | }, 27 | "aiohttp": { 28 | "hashes": [ 29 | "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe", 30 | "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe", 31 | "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5", 32 | "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8", 33 | "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd", 34 | "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb", 35 | "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c", 36 | "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87", 37 | "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0", 38 | "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290", 39 | "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5", 40 | "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287", 41 | "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde", 42 | "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf", 43 | "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8", 44 | "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16", 45 | "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf", 46 | "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809", 47 | "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213", 48 | "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f", 49 | "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013", 50 | "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b", 51 | "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9", 52 | "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5", 53 | "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb", 54 | "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df", 55 | "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4", 56 | "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439", 57 | "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f", 58 | "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22", 59 | "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f", 60 | "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5", 61 | "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970", 62 | "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009", 63 | "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc", 64 | "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", 65 | "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" 66 | ], 67 | "markers": "python_version >= '3.5.2'", 68 | "version": "==3.7.4.post0" 69 | }, 70 | "async-timeout": { 71 | "hashes": [ 72 | "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", 73 | "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" 74 | ], 75 | "version": "==3.0.1" 76 | }, 77 | "attrs": { 78 | "hashes": [ 79 | "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", 80 | "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" 81 | ], 82 | "version": "==21.2.0" 83 | }, 84 | "ccxt": { 85 | "hashes": [ 86 | "sha256:529367b7b11a38b6f145bb2836deb869b91250cac54309fce67a1792f2320cf4", 87 | "sha256:c3a36b10b004da18eca7187ddffbd531c11cf5cfd1a57a0fbe2c1786d192146d" 88 | ], 89 | "index": "pypi", 90 | "version": "==1.57.43" 91 | }, 92 | "certifi": { 93 | "hashes": [ 94 | "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", 95 | "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" 96 | ], 97 | "version": "==2021.10.8" 98 | }, 99 | "cffi": { 100 | "hashes": [ 101 | "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", 102 | "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", 103 | "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", 104 | "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", 105 | "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", 106 | "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", 107 | "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", 108 | "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", 109 | "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", 110 | "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", 111 | "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", 112 | "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", 113 | "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", 114 | "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", 115 | "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", 116 | "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", 117 | "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", 118 | "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", 119 | "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", 120 | "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", 121 | "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", 122 | "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", 123 | "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", 124 | "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", 125 | "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", 126 | "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", 127 | "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", 128 | "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", 129 | "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", 130 | "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", 131 | "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", 132 | "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", 133 | "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", 134 | "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", 135 | "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", 136 | "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", 137 | "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", 138 | "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", 139 | "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", 140 | "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", 141 | "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", 142 | "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", 143 | "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", 144 | "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", 145 | "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", 146 | "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", 147 | "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", 148 | "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", 149 | "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", 150 | "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" 151 | ], 152 | "version": "==1.15.0" 153 | }, 154 | "chardet": { 155 | "hashes": [ 156 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", 157 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" 158 | ], 159 | "version": "==4.0.0" 160 | }, 161 | "charset-normalizer": { 162 | "hashes": [ 163 | "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", 164 | "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" 165 | ], 166 | "markers": "python_version >= '3'", 167 | "version": "==2.0.7" 168 | }, 169 | "cryptography": { 170 | "hashes": [ 171 | "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6", 172 | "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6", 173 | "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c", 174 | "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999", 175 | "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e", 176 | "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992", 177 | "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d", 178 | "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588", 179 | "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa", 180 | "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d", 181 | "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd", 182 | "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d", 183 | "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953", 184 | "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2", 185 | "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8", 186 | "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6", 187 | "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9", 188 | "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6", 189 | "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad", 190 | "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76" 191 | ], 192 | "version": "==35.0.0" 193 | }, 194 | "idna": { 195 | "hashes": [ 196 | "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", 197 | "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" 198 | ], 199 | "markers": "python_version >= '3'", 200 | "version": "==3.3" 201 | }, 202 | "idna-ssl": { 203 | "hashes": [ 204 | "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" 205 | ], 206 | "markers": "python_version < '3.7'", 207 | "version": "==1.1.0" 208 | }, 209 | "joblib": { 210 | "hashes": [ 211 | "sha256:4158fcecd13733f8be669be0683b96ebdbbd38d23559f54dca7205aea1bf1e35", 212 | "sha256:f21f109b3c7ff9d95f8387f752d0d9c34a02aa2f7060c2135f465da0e5160ff6" 213 | ], 214 | "index": "pypi", 215 | "version": "==1.1.0" 216 | }, 217 | "multidict": { 218 | "hashes": [ 219 | "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b", 220 | "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031", 221 | "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0", 222 | "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce", 223 | "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda", 224 | "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858", 225 | "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5", 226 | "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8", 227 | "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22", 228 | "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac", 229 | "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e", 230 | "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6", 231 | "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5", 232 | "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0", 233 | "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11", 234 | "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a", 235 | "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55", 236 | "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341", 237 | "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b", 238 | "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704", 239 | "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b", 240 | "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1", 241 | "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621", 242 | "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d", 243 | "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5", 244 | "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7", 245 | "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac", 246 | "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d", 247 | "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef", 248 | "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0", 249 | "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f", 250 | "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02", 251 | "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b", 252 | "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37", 253 | "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23", 254 | "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d", 255 | "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065", 256 | "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86", 257 | "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6", 258 | "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded", 259 | "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4", 260 | "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7", 261 | "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a", 262 | "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17", 263 | "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3", 264 | "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21", 265 | "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24", 266 | "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940", 267 | "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac", 268 | "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c", 269 | "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422", 270 | "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628", 271 | "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0", 272 | "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf", 273 | "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e", 274 | "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677", 275 | "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f", 276 | "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c", 277 | "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4", 278 | "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b", 279 | "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747", 280 | "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0", 281 | "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01", 282 | "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8", 283 | "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9", 284 | "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64", 285 | "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d", 286 | "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0", 287 | "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52", 288 | "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1", 289 | "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae", 290 | "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d" 291 | ], 292 | "version": "==5.2.0" 293 | }, 294 | "numpy": { 295 | "hashes": [ 296 | "sha256:012426a41bc9ab63bb158635aecccc7610e3eff5d31d1eb43bc099debc979d94", 297 | "sha256:06fab248a088e439402141ea04f0fffb203723148f6ee791e9c75b3e9e82f080", 298 | "sha256:0eef32ca3132a48e43f6a0f5a82cb508f22ce5a3d6f67a8329c81c8e226d3f6e", 299 | "sha256:1ded4fce9cfaaf24e7a0ab51b7a87be9038ea1ace7f34b841fe3b6894c721d1c", 300 | "sha256:2e55195bc1c6b705bfd8ad6f288b38b11b1af32f3c8289d6c50d47f950c12e76", 301 | "sha256:2ea52bd92ab9f768cc64a4c3ef8f4b2580a17af0a5436f6126b08efbd1838371", 302 | "sha256:36674959eed6957e61f11c912f71e78857a8d0604171dfd9ce9ad5cbf41c511c", 303 | "sha256:384ec0463d1c2671170901994aeb6dce126de0a95ccc3976c43b0038a37329c2", 304 | "sha256:39b70c19ec771805081578cc936bbe95336798b7edf4732ed102e7a43ec5c07a", 305 | "sha256:400580cbd3cff6ffa6293df2278c75aef2d58d8d93d3c5614cd67981dae68ceb", 306 | "sha256:43d4c81d5ffdff6bae58d66a3cd7f54a7acd9a0e7b18d97abb255defc09e3140", 307 | "sha256:50a4a0ad0111cc1b71fa32dedd05fa239f7fb5a43a40663269bb5dc7877cfd28", 308 | "sha256:603aa0706be710eea8884af807b1b3bc9fb2e49b9f4da439e76000f3b3c6ff0f", 309 | "sha256:6149a185cece5ee78d1d196938b2a8f9d09f5a5ebfbba66969302a778d5ddd1d", 310 | "sha256:759e4095edc3c1b3ac031f34d9459fa781777a93ccc633a472a5468587a190ff", 311 | "sha256:7fb43004bce0ca31d8f13a6eb5e943fa73371381e53f7074ed21a4cb786c32f8", 312 | "sha256:811daee36a58dc79cf3d8bdd4a490e4277d0e4b7d103a001a4e73ddb48e7e6aa", 313 | "sha256:8b5e972b43c8fc27d56550b4120fe6257fdc15f9301914380b27f74856299fea", 314 | "sha256:99abf4f353c3d1a0c7a5f27699482c987cf663b1eac20db59b8c7b061eabd7fc", 315 | "sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73", 316 | "sha256:a12ff4c8ddfee61f90a1633a4c4afd3f7bcb32b11c52026c92a12e1325922d0d", 317 | "sha256:a4646724fba402aa7504cd48b4b50e783296b5e10a524c7a6da62e4a8ac9698d", 318 | "sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4", 319 | "sha256:a9d17f2be3b427fbb2bce61e596cf555d6f8a56c222bd2ca148baeeb5e5c783c", 320 | "sha256:ab83f24d5c52d60dbc8cd0528759532736b56db58adaa7b5f1f76ad551416a1e", 321 | "sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea", 322 | "sha256:c843b3f50d1ab7361ca4f0b3639bf691569493a56808a0b0c54a051d260b7dbd", 323 | "sha256:cae865b1cae1ec2663d8ea56ef6ff185bad091a5e33ebbadd98de2cfa3fa668f", 324 | "sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff", 325 | "sha256:cf2402002d3d9f91c8b01e66fbb436a4ed01c6498fffed0e4c7566da1d40ee1e", 326 | "sha256:d051ec1c64b85ecc69531e1137bb9751c6830772ee5c1c426dbcfe98ef5788d7", 327 | "sha256:d6631f2e867676b13026e2846180e2c13c1e11289d67da08d71cacb2cd93d4aa", 328 | "sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827", 329 | "sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60" 330 | ], 331 | "version": "==1.19.5" 332 | }, 333 | "pandas": { 334 | "hashes": [ 335 | "sha256:0a643bae4283a37732ddfcecab3f62dd082996021b980f580903f4e8e01b3c5b", 336 | "sha256:0de3ddb414d30798cbf56e642d82cac30a80223ad6fe484d66c0ce01a84d6f2f", 337 | "sha256:19a2148a1d02791352e9fa637899a78e371a3516ac6da5c4edc718f60cbae648", 338 | "sha256:21b5a2b033380adbdd36b3116faaf9a4663e375325831dac1b519a44f9e439bb", 339 | "sha256:24c7f8d4aee71bfa6401faeba367dd654f696a77151a8a28bc2013f7ced4af98", 340 | "sha256:26fa92d3ac743a149a31b21d6f4337b0594b6302ea5575b37af9ca9611e8981a", 341 | "sha256:2860a97cbb25444ffc0088b457da0a79dc79f9c601238a3e0644312fcc14bf11", 342 | "sha256:2b1c6cd28a0dfda75c7b5957363333f01d370936e4c6276b7b8e696dd500582a", 343 | "sha256:2c2f7c670ea4e60318e4b7e474d56447cf0c7d83b3c2a5405a0dbb2600b9c48e", 344 | "sha256:3be7a7a0ca71a2640e81d9276f526bca63505850add10206d0da2e8a0a325dae", 345 | "sha256:4c62e94d5d49db116bef1bd5c2486723a292d79409fc9abd51adf9e05329101d", 346 | "sha256:5008374ebb990dad9ed48b0f5d0038124c73748f5384cc8c46904dace27082d9", 347 | "sha256:5447ea7af4005b0daf695a316a423b96374c9c73ffbd4533209c5ddc369e644b", 348 | "sha256:573fba5b05bf2c69271a32e52399c8de599e4a15ab7cec47d3b9c904125ab788", 349 | "sha256:5a780260afc88268a9d3ac3511d8f494fdcf637eece62fb9eb656a63d53eb7ca", 350 | "sha256:70865f96bb38fec46f7ebd66d4b5cfd0aa6b842073f298d621385ae3898d28b5", 351 | "sha256:731568be71fba1e13cae212c362f3d2ca8932e83cb1b85e3f1b4dd77d019254a", 352 | "sha256:b61080750d19a0122469ab59b087380721d6b72a4e7d962e4d7e63e0c4504814", 353 | "sha256:bf23a3b54d128b50f4f9d4675b3c1857a688cc6731a32f931837d72effb2698d", 354 | "sha256:c16d59c15d946111d2716856dd5479221c9e4f2f5c7bc2d617f39d870031e086", 355 | "sha256:c61c043aafb69329d0f961b19faa30b1dab709dd34c9388143fc55680059e55a", 356 | "sha256:c94ff2780a1fd89f190390130d6d36173ca59fcfb3fe0ff596f9a56518191ccb", 357 | "sha256:edda9bacc3843dfbeebaf7a701763e68e741b08fccb889c003b0a52f0ee95782", 358 | "sha256:f10fc41ee3c75a474d3bdf68d396f10782d013d7f67db99c0efbfd0acb99701b" 359 | ], 360 | "index": "pypi", 361 | "version": "==1.1.5" 362 | }, 363 | "pycares": { 364 | "hashes": [ 365 | "sha256:09b28fc7bc2cc05f7f69bf1636ddf46086e0a1837b62961e2092fcb40477320d", 366 | "sha256:0aa97f900a7ffb259be77d640006585e2a907b0cd4edeee0e85cf16605995d5a", 367 | "sha256:103353577a6266a53e71bfee4cf83825f1401fefa60f0fb8bdec35f13be6a5f2", 368 | "sha256:1b959dd5921d207d759d421eece1b60416df33a7f862465739d5f2c363c2f523", 369 | "sha256:26e67e4f81c80a5955dcf6193f3d9bee3c491fc0056299b383b84d792252fba4", 370 | "sha256:38e54037f36c149146ff15f17a4a963fbdd0f9871d4a21cd94ff9f368140f57e", 371 | "sha256:3c7fb8d34ee11971c39acfaf98d0fac66725385ccef3bfe1b174c92b210e1aa4", 372 | "sha256:3d5e50c95849f6905d2a9dbf02ed03f82580173e3c5604a39e2ad054185631f1", 373 | "sha256:44896d6e191a6b5a914dbe3aa7c748481bf6ad19a9df33c1e76f8f2dc33fc8f0", 374 | "sha256:4876fc790ae32832ae270c4a010a1a77e12ddf8d8e6ad70ad0b0a9d506c985f7", 375 | "sha256:53bc4f181b19576499b02cea4b45391e8dcbe30abd4cd01492f66bfc15615a13", 376 | "sha256:57315b8eb8fdbc56b3ad4932bc4b17132bb7c7fd2bd590f7fb84b6b522098aa9", 377 | "sha256:615406013cdcd1b445e5d1a551d276c6200b3abe77e534f8a7f7e1551208d14f", 378 | "sha256:6580aef5d1b29a88c3d72fe73c691eacfd454f86e74d3fdd18f4bad8e8def98b", 379 | "sha256:6f258c1b74c048a9501a25f732f11b401564005e5e3c18f1ca6cad0c3dc0fb19", 380 | "sha256:7661d6bbd51a337e7373cb356efa8be9b4655fda484e068f9455e939aec8d54e", 381 | "sha256:82b3259cb590ddd107a6d2dc52da2a2e9a986bf242e893d58c786af2f8191047", 382 | "sha256:8ebb3ba0485f66cae8eed7ce3e9ed6f2c0bfd5e7319d5d0fbbb511064f17e1d4", 383 | "sha256:a34b0e3e693dceb60b8a1169668d606c75cb100ceba0a2df53c234a0eb067fbc", 384 | "sha256:ad6caf580ee69806fc6534be93ddbb6e99bf94296d79ab351c37b2992b17abfd", 385 | "sha256:b17ef48729786e62b574c6431f675f4cb02b27691b49e7428a605a50cd59c072", 386 | "sha256:c5362b7690ca481440f6b98395ac6df06aa50518ccb183c560464d1e5e2ab5d4", 387 | "sha256:c95c964d5dd307e104b44b193095c67bb6b10c9eda1ffe7d44ab7a9e84c476d9", 388 | "sha256:cd3011ffd5e1ad55880f7256791dbab9c43ebeda260474a968f19cd0319e1aef", 389 | "sha256:d0154fc5753b088758fbec9bc137e1b24bb84fc0c6a09725c8bac25a342311cd", 390 | "sha256:d4a5081e232c1d181883dcac4675807f3a6cf33911c4173fbea00c0523687ed4", 391 | "sha256:d52f9c725d2a826d5ffa37681eb07ffb996bfe21788590ef257664a3898fc0b5", 392 | "sha256:db5a533111a3cfd481e7e4fb2bf8bef69f4fa100339803e0504dd5aecafb96a5", 393 | "sha256:dca9dc58845a9d083f302732a3130c68ded845ad5d463865d464e53c75a3dd45", 394 | "sha256:e9773e07684a55f54657df05237267611a77b294ec3bacb5f851c4ffca38a465", 395 | "sha256:eb60be66accc9a9ea1018b591a1f5800cba83491d07e9acc8c56bc6e6607ab54", 396 | "sha256:f60c04c5561b1ddf85ca4e626943cc09d7fb684e1adb22abb632095415a40fd7", 397 | "sha256:fdff88393c25016f417770d82678423fc7a56995abb2df3d2a1e55725db6977d" 398 | ], 399 | "version": "==4.0.0" 400 | }, 401 | "pycparser": { 402 | "hashes": [ 403 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", 404 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" 405 | ], 406 | "version": "==2.20" 407 | }, 408 | "python-dateutil": { 409 | "hashes": [ 410 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 411 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 412 | ], 413 | "version": "==2.8.2" 414 | }, 415 | "pytz": { 416 | "hashes": [ 417 | "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", 418 | "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" 419 | ], 420 | "version": "==2021.3" 421 | }, 422 | "requests": { 423 | "hashes": [ 424 | "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", 425 | "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" 426 | ], 427 | "version": "==2.26.0" 428 | }, 429 | "six": { 430 | "hashes": [ 431 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 432 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 433 | ], 434 | "index": "pypi", 435 | "version": "==1.16.0" 436 | }, 437 | "typing-extensions": { 438 | "hashes": [ 439 | "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", 440 | "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", 441 | "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" 442 | ], 443 | "markers": "python_version < '3.8'", 444 | "version": "==3.10.0.2" 445 | }, 446 | "urllib3": { 447 | "hashes": [ 448 | "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", 449 | "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" 450 | ], 451 | "version": "==1.26.7" 452 | }, 453 | "yarl": { 454 | "hashes": [ 455 | "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e", 456 | "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434", 457 | "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366", 458 | "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3", 459 | "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec", 460 | "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959", 461 | "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e", 462 | "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c", 463 | "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6", 464 | "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a", 465 | "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6", 466 | "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424", 467 | "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e", 468 | "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f", 469 | "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50", 470 | "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2", 471 | "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc", 472 | "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4", 473 | "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970", 474 | "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10", 475 | "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0", 476 | "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406", 477 | "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896", 478 | "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643", 479 | "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721", 480 | "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478", 481 | "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724", 482 | "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e", 483 | "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8", 484 | "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96", 485 | "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25", 486 | "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76", 487 | "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2", 488 | "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2", 489 | "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c", 490 | "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", 491 | "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" 492 | ], 493 | "markers": "python_version >= '3.5.2'", 494 | "version": "==1.6.3" 495 | } 496 | }, 497 | "develop": {} 498 | } 499 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | crypto_data_fetcher 2 | 3 | ## Features 4 | 5 | - fetch (ohlcv/funding rate) as pandas DataFrame 6 | - incremental fetch 7 | 8 | ## Install 9 | 10 | ```bash 11 | pip install "git+https://github.com/richmanbtc/crypto_data_fetcher.git@v0.0.18#egg=crypto_data_fetcher" 12 | ``` 13 | 14 | ## Usage 15 | 16 | 17 | 18 | ## Supported exchange and data 19 | 20 | |exchange|market_type|ohlcv|mark ohlcv|funding rate|premium index| 21 | |:-:|:-:|:-:|:-:|:-:|:-:| 22 | |binance|spot|o|x|x|x| 23 | |binance|future|o|o|o|x| 24 | |bybit|inverse|o|o|o|o| 25 | |bybit|usdt|o|o|o|o| 26 | |ftx|all|o|o|o|x| 27 | |kraken|spot|o|x|x|x| 28 | 29 | ## License 30 | 31 | CC0 32 | 33 | ## Developer 34 | 35 | ### test 36 | 37 | ```bash 38 | python3 -m unittest tests/test_* 39 | ``` 40 | 41 | ```bash 42 | pyenv exec pipenv run python3 -m unittest tests/test_* 43 | ``` 44 | -------------------------------------------------------------------------------- /crypto_data_fetcher/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richmanbtc/crypto_data_fetcher/3471c8ea095f85bcd6c4d3207ce2d41af485a59d/crypto_data_fetcher/__init__.py -------------------------------------------------------------------------------- /crypto_data_fetcher/binance_future.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from .utils import smart_append, create_null_logger, normalize_to_unix 3 | 4 | class BinanceFutureFetcher: 5 | def __init__(self, logger=None, ccxt_client=None): 6 | self.logger = create_null_logger() if logger is None else logger 7 | self.ccxt_client = ccxt_client 8 | 9 | def fetch_ohlcv(self, df=None, start_time=None, interval_sec=None, market=None, price_type=None): 10 | if price_type is not None: 11 | raise Exception('price_type {} not implemented'.format(price_type)) 12 | 13 | limit = 1500 14 | 15 | if start_time: 16 | from_time = int(normalize_to_unix(start_time) * 1000) 17 | else: 18 | from_time = 1 19 | 20 | if df is not None and df.shape[0]: 21 | from_time = int(df.index.max().timestamp() * 1000) + 1 22 | 23 | dfs = [] 24 | 25 | while True: 26 | data = self.ccxt_client.fapiPublicGetKlines({ 27 | 'symbol': market, 28 | 'startTime': from_time, 29 | 'interval': format_interval_sec(interval_sec), 30 | 'limit': limit 31 | }) 32 | if len(data) == 0: 33 | break 34 | 35 | df2 = pd.DataFrame(data, columns=[ 36 | 'timestamp', 37 | 'op', 38 | 'hi', 39 | 'lo', 40 | 'cl', 41 | 'volume', 42 | 'close_time', 43 | 'quote_asset_volume', 44 | 'trades', 45 | 'taker_buy_base_asset_volume', 46 | 'taker_buy_quote_asset_volume', 47 | 'ignore', 48 | ])[['timestamp', 'op', 'hi', 'lo', 'cl', 'volume']] 49 | 50 | # binanceはfrom_time未満が返ることがあるので 51 | df2 = df2[from_time <= df2['timestamp'].astype('int64')] 52 | if df2.shape[0] == 0: 53 | break 54 | 55 | df2['timestamp'] = pd.to_datetime(df2['timestamp'], unit='ms', utc=True) 56 | 57 | for col in ['op', 'hi', 'lo', 'cl', 'volume']: 58 | df2[col] = df2[col].astype('float64') 59 | 60 | from_time = int(df2['timestamp'].max().timestamp() * 1000) + 1 61 | dfs.append(df2) 62 | 63 | if len(dfs) == 0: 64 | return None if df is None else df.copy() 65 | else: 66 | df = smart_append(df, pd.concat(dfs).set_index('timestamp')) 67 | # 最後は未確定足なので削除 68 | df = df[df.index != df.index.max()] 69 | return df 70 | 71 | # not implemented 72 | # def fetch_fr(self, df=None, start_time=None, market=None): 73 | # pass 74 | 75 | def format_interval_sec(interval_sec): 76 | interval_min = interval_sec // 60 77 | if interval_min < 60: 78 | return '{}m'.format(interval_min) 79 | if interval_min < 24 * 60: 80 | return '{}h'.format(interval_min // 60) 81 | else: 82 | return '{}d'.format(interval_min // (24 * 60)) 83 | -------------------------------------------------------------------------------- /crypto_data_fetcher/binance_spot.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from .utils import smart_append, create_null_logger, normalize_to_unix 3 | 4 | class BinanceSpotFetcher: 5 | def __init__(self, logger=None, ccxt_client=None): 6 | self.logger = create_null_logger() if logger is None else logger 7 | self.ccxt_client = ccxt_client 8 | 9 | def fetch_ohlcv(self, df=None, start_time=None, interval_sec=None, market=None, price_type=None): 10 | if price_type is not None: 11 | raise Exception('price_type {} not implemented'.format(price_type)) 12 | 13 | limit = 1500 14 | 15 | if start_time: 16 | from_time = int(normalize_to_unix(start_time) * 1000) 17 | else: 18 | from_time = 1 19 | 20 | if df is not None and df.shape[0]: 21 | from_time = int(df.index.max().timestamp() * 1000) + 1 22 | 23 | dfs = [] 24 | 25 | while True: 26 | data = self.ccxt_client.publicGetKlines({ 27 | 'symbol': market, 28 | 'startTime': from_time, 29 | 'interval': format_interval_sec(interval_sec), 30 | 'limit': limit 31 | }) 32 | if len(data) == 0: 33 | break 34 | 35 | df2 = pd.DataFrame(data, columns=[ 36 | 'timestamp', 37 | 'op', 38 | 'hi', 39 | 'lo', 40 | 'cl', 41 | 'volume', 42 | 'close_time', 43 | 'quote_asset_volume', 44 | 'trades', 45 | 'taker_buy_base_asset_volume', 46 | 'taker_buy_quote_asset_volume', 47 | 'ignore', 48 | ])[['timestamp', 'op', 'hi', 'lo', 'cl', 'volume']] 49 | 50 | # binanceはfrom_time未満が返ることがあるので 51 | df2 = df2[from_time <= df2['timestamp'].astype('int64')] 52 | if df2.shape[0] == 0: 53 | break 54 | 55 | df2['timestamp'] = pd.to_datetime(df2['timestamp'], unit='ms', utc=True) 56 | 57 | for col in ['op', 'hi', 'lo', 'cl', 'volume']: 58 | df2[col] = df2[col].astype('float64') 59 | 60 | from_time = int(df2['timestamp'].max().timestamp() * 1000) + 1 61 | dfs.append(df2) 62 | 63 | if len(dfs) == 0: 64 | return None if df is None else df.copy() 65 | else: 66 | df = smart_append(df, pd.concat(dfs).set_index('timestamp')) 67 | # 最後は未確定足なので削除 68 | df = df[df.index != df.index.max()] 69 | return df 70 | 71 | # not implemented 72 | # def fetch_fr(self, df=None, start_time=None, market=None): 73 | # pass 74 | 75 | def format_interval_sec(interval_sec): 76 | interval_min = interval_sec // 60 77 | if interval_min < 60: 78 | return '{}m'.format(interval_min) 79 | if interval_min < 24 * 60: 80 | return '{}h'.format(interval_min // 60) 81 | else: 82 | return '{}d'.format(interval_min // (24 * 60)) 83 | -------------------------------------------------------------------------------- /crypto_data_fetcher/bybit.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from .utils import smart_append, create_null_logger, normalize_to_unix 3 | 4 | class BybitFetcher: 5 | def __init__(self, logger=None, ccxt_client=None): 6 | self.logger = create_null_logger() if logger is None else logger 7 | self.ccxt_client = ccxt_client 8 | 9 | def fetch_ohlcv(self, df=None, start_time=None, interval_sec=None, market=None, price_type=None): 10 | limit = 200 11 | 12 | if start_time: 13 | from_time = int(normalize_to_unix(start_time)) 14 | else: 15 | from_time = 1 16 | 17 | if df is not None and df.shape[0]: 18 | from_time = int(df.index.max().timestamp()) + 1 19 | 20 | interval = { 21 | 1: 'D', 22 | 7: 'W', 23 | }.get(interval_sec // (24 * 60 * 60)) or interval_sec // 60 24 | 25 | is_linear = market.endswith('USDT') 26 | 27 | dfs = [] 28 | 29 | while True: 30 | self.logger.debug('{} {}'.format(market, from_time)) 31 | 32 | if price_type == 'mark': 33 | if is_linear: 34 | data = self.ccxt_client.publicLinearGetMarkPriceKline({ 35 | 'symbol': market, 36 | 'from': from_time, 37 | 'interval': interval, 38 | 'limit': limit 39 | })['result'] 40 | else: 41 | data = self.ccxt_client.v2PublicGetMarkPriceKline({ 42 | 'symbol': market, 43 | 'from': from_time, 44 | 'interval': interval, 45 | 'limit': limit 46 | })['result'] 47 | elif price_type == 'index': 48 | if is_linear: 49 | data = self.ccxt_client.publicLinearGetIndexPriceKline({ 50 | 'symbol': market, 51 | 'from': from_time, 52 | 'interval': interval, 53 | 'limit': limit 54 | })['result'] 55 | else: 56 | data = self.ccxt_client.v2PublicGetIndexPriceKline({ 57 | 'symbol': market, 58 | 'from': from_time, 59 | 'interval': interval, 60 | 'limit': limit 61 | })['result'] 62 | elif price_type == 'premium_index': 63 | if is_linear: 64 | data = self.ccxt_client.publicLinearGetPremiumIndexKline({ 65 | 'symbol': market, 66 | 'from': from_time, 67 | 'interval': interval, 68 | 'limit': limit 69 | })['result'] 70 | else: 71 | data = self.ccxt_client.v2PublicGetPremiumIndexKline({ 72 | 'symbol': market, 73 | 'from': from_time, 74 | 'interval': interval, 75 | 'limit': limit 76 | })['result'] 77 | else: 78 | if is_linear: 79 | data = self.ccxt_client.publicLinearGetKline({ 80 | 'symbol': market, 81 | 'from': from_time, 82 | 'interval': interval, 83 | 'limit': limit 84 | })['result'] 85 | else: 86 | data = self.ccxt_client.v2PublicGetKlineList({ 87 | 'symbol': market, 88 | 'from': from_time, 89 | 'interval': interval, 90 | 'limit': limit 91 | })['result'] 92 | 93 | if data is None or len(data) <= 1: # 最後を取り除くので最低2個必要 94 | break 95 | 96 | # self.logger.debug(data) 97 | 98 | df2 = pd.DataFrame(data) 99 | columns = ['timestamp', 'op', 'hi', 'lo', 'cl'] 100 | if price_type is None: 101 | columns.append('volume') 102 | if 'open_time' in df2.columns and 'start_at' in df2.columns: 103 | df2 = df2.drop(columns=['start_at']) 104 | df2 = df2.rename(columns={ 105 | 'open': 'op', 106 | 'high': 'hi', 107 | 'low': 'lo', 108 | 'close': 'cl', 109 | 'open_time': 'timestamp', 110 | 'start_at': 'timestamp', 111 | })[columns] 112 | df2['timestamp'] = pd.to_datetime(df2['timestamp'], unit='s', utc=True) 113 | 114 | # 最後は未確定足なので削除 115 | df2 = df2[df2['timestamp'] != df2['timestamp'].max()] 116 | 117 | for col in ['op', 'hi', 'lo', 'cl', 'volume']: 118 | if col in columns: 119 | df2[col] = df2[col].astype('float64') 120 | 121 | from_time = int(df2['timestamp'].max().timestamp()) + 1 122 | dfs.append(df2) 123 | 124 | if len(data) < limit: 125 | break 126 | 127 | if len(dfs) == 0: 128 | return None if df is None else df.copy() 129 | else: 130 | return smart_append(df, pd.concat(dfs).set_index('timestamp')) 131 | 132 | # df_premium_index has to be minutes data for accurate fr 133 | def calc_fr_from_premium_index(self, df_premium_index=None): 134 | df_premium_index = df_premium_index.reset_index() 135 | 136 | df_premium_index['timestamp'] = df_premium_index['timestamp'].dt.floor('8H') + pd.to_timedelta(2 * 8, unit='hour') 137 | df2 = pd.concat([ 138 | df_premium_index.groupby('timestamp')['cl'].mean() 139 | ], axis=1) 140 | 141 | interest_rate = (0.0006 - 0.0003) / 3.0 142 | df2['fr'] = df2['cl'] + (interest_rate - df2['cl']).clip(-0.0005, 0.0005) 143 | df2 = df2[['fr']] 144 | return df2 145 | -------------------------------------------------------------------------------- /crypto_data_fetcher/ftx.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | import pandas as pd 4 | from .utils import smart_append, create_null_logger, normalize_to_unix 5 | 6 | class FtxFetcher: 7 | def __init__(self, logger=None, ccxt_client=None): 8 | self.logger = create_null_logger() if logger is None else logger 9 | self.ccxt_client = ccxt_client 10 | 11 | def fetch_ohlcv(self, df=None, start_time=None, interval_sec=None, market=None, price_type=None): 12 | limit = 5000 13 | 14 | if df is not None and df.shape[0]: 15 | from_time = int(df.index.max().timestamp()) + interval_sec 16 | elif start_time: 17 | from_time = normalize_to_unix(start_time) 18 | else: 19 | from_time = self._find_start_time(market=market) 20 | 21 | # ftxは最近のデータが返ってくるから、最初から取得するには、end_timeをstart_time + interval * limitに設定する必要がある 22 | # そうすると、データが足りないことを終了判定に使えないから、データが足りないときはfrom_timeを進める 23 | # from_timeが現在を超えたら終了 24 | # end_timeが現在時刻を超えたら何も返らないので注意 (expired futureは現在時刻の代わりにfuture期限) 25 | 26 | # 普通に未来から過去へ取得する方法でも良いかもしれない(okexはそう実装した)。 27 | 28 | dfs = [] 29 | 30 | total_end_time = self._find_total_end_time(market=market) 31 | 32 | while from_time < total_end_time: 33 | end_time = from_time + interval_sec * limit 34 | end_time = min([end_time, total_end_time]) # 未来時刻だと何も返らないので 35 | self.logger.debug('{} {} {}'.format(market, from_time, end_time)) 36 | 37 | if price_type == 'index': 38 | data = self.ccxt_client.publicGetIndexesMarketNameCandles({ 39 | 'market_name': market.replace('-PERP', ''), 40 | 'start_time': from_time, 41 | 'end_time': end_time - 1, # キャッシュを無効にするために必要。境界値を含む仕様っぽいので含まないように調整 42 | 'resolution': interval_sec, 43 | 'limit': limit 44 | })['result'] 45 | elif price_type is None: 46 | data = self.ccxt_client.publicGetMarketsMarketNameCandles({ 47 | 'market_name': market, 48 | 'start_time': from_time, 49 | 'end_time': end_time - 1, # キャッシュを無効にするために必要。境界値を含む仕様っぽいので含まないように調整 50 | 'resolution': interval_sec, 51 | 'limit': limit 52 | })['result'] 53 | else: 54 | raise Exception('unknown price_type {}'.format(price_type)) 55 | 56 | from_time = end_time 57 | 58 | if len(data) <= 0: 59 | self.logger.debug('len(data) <= 1') 60 | continue 61 | 62 | # self.logger.debug(data) 63 | 64 | df2 = pd.DataFrame(data) 65 | columns = ['timestamp', 'op', 'hi', 'lo', 'cl'] 66 | if price_type is None: 67 | columns.append('volume') 68 | df2 = df2.rename(columns={ 69 | 'open': 'op', 70 | 'high': 'hi', 71 | 'low': 'lo', 72 | 'close': 'cl', 73 | 'startTime': 'timestamp', 74 | })[columns] 75 | df2['timestamp'] = pd.to_datetime(df2['timestamp'], utc=True) 76 | 77 | for col in ['op', 'hi', 'lo', 'cl', 'volume']: 78 | if col in columns: 79 | df2[col] = df2[col].astype('float64') 80 | 81 | dfs.append(df2) 82 | 83 | if len(dfs) == 0: 84 | return None if df is None else df.copy() 85 | else: 86 | df = smart_append(df, pd.concat(dfs).set_index('timestamp')) 87 | # 最後は未確定足なので削除 88 | df = df[df.index != df.index.max()] 89 | return df 90 | 91 | def fetch_fr(self, df=None, start_time=None, market=None): 92 | limit = 1000 # undocumented 93 | interval_sec = 60 * 60 94 | 95 | if df is not None and df.shape[0]: 96 | from_time = int(df.index.max().timestamp()) + interval_sec 97 | elif start_time: 98 | from_time = normalize_to_unix(start_time) 99 | else: 100 | from_time = self._find_start_time(market=market) 101 | 102 | # ftxは最近のデータが返ってくるから、最初から取得するには、end_timeをstart_time + interval * limitに設定する必要がある 103 | # そうすると、データが足りないことを終了判定に使えないから、データが足りないときはfrom_timeを進める 104 | # from_timeが現在を超えたら終了 105 | 106 | dfs = [] 107 | 108 | total_end_time = self._find_total_end_time(market=market) 109 | 110 | while from_time < total_end_time: 111 | end_time = from_time + interval_sec * limit 112 | end_time = min([end_time, total_end_time]) # 未来時刻だと何も返らないので 113 | self.logger.debug('{} {} {}'.format(market, from_time, end_time)) 114 | 115 | data = self.ccxt_client.publicGetFundingRates({ 116 | 'future': market, 117 | 'start_time': from_time, 118 | 'end_time': end_time - 1, # キャッシュを無効にするために必要。境界値を含む仕様っぽいので含まないように調整 119 | 'limit': limit 120 | })['result'] 121 | 122 | from_time = end_time 123 | 124 | if len(data) <= 0: 125 | self.logger.debug('len(data) <= 1') 126 | continue 127 | 128 | # self.logger.debug(data) 129 | 130 | df2 = pd.DataFrame(data) 131 | df2 = df2.rename(columns={ 132 | 'rate': 'fr', 133 | 'time': 'timestamp', 134 | })[['fr', 'timestamp']] 135 | df2['timestamp'] = pd.to_datetime(df2['timestamp'], utc=True) 136 | 137 | for col in ['fr']: 138 | df2[col] = df2[col].astype('float64') 139 | 140 | dfs.append(df2) 141 | 142 | if len(dfs) == 0: 143 | return None if df is None else df.copy() 144 | else: 145 | return smart_append(df, pd.concat(dfs).set_index('timestamp')) 146 | 147 | def fetch_my_trades(self, df=None, start_time=None, market=None): 148 | limit = 5000 # undocumented 149 | 150 | if df is not None and df.shape[0]: 151 | from_time = math.floor(df.index.max().timestamp()) 152 | elif start_time: 153 | from_time = normalize_to_unix(start_time) 154 | else: 155 | from_time = self._find_start_time(market=market) 156 | 157 | # ftxは最近のデータが返ってくるから、未来から過去へ取得 158 | 159 | dfs = [] 160 | 161 | total_end_time = self._find_total_end_time(market=market) 162 | end_time = total_end_time 163 | 164 | while from_time < end_time: 165 | self.logger.debug('{} {} {}'.format(market, from_time, end_time)) 166 | 167 | data = self.ccxt_client.fetch_my_trades(market, params={ 168 | 'start_time': from_time, 169 | 'end_time': end_time, # キャッシュを無効にするために必要。多分、境界値を含む仕様。漏らさないように重複させて取得 170 | 'limit': limit 171 | }) 172 | 173 | if len(data) <= 0: 174 | self.logger.debug('len(data) <= 1') 175 | continue 176 | 177 | df2 = pd.json_normalize(data, sep='_') 178 | df2 = df2.rename(columns={ 179 | })[['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost', 'fee_cost', 'fee_currency', 'fee_rate']] 180 | df2['timestamp'] = pd.to_datetime(df2['timestamp'], unit='ms', utc=True) 181 | 182 | end_time = math.ceil(df2['timestamp'].min().timestamp()) 183 | 184 | dfs.append(df2) 185 | 186 | if len(data) <= 10: 187 | break 188 | 189 | if len(dfs) == 0: 190 | return None if df is None else df.copy() 191 | else: 192 | return smart_append(df, pd.concat(dfs).set_index('id')) 193 | 194 | def _find_start_time(self, market=None): 195 | limit = 5000 196 | 197 | data = self.ccxt_client.publicGetMarketsMarketNameCandles({ 198 | 'market_name': market, 199 | 'resolution': 24 * 60 * 60, 200 | 'limit': limit 201 | })['result'] 202 | 203 | if len(data) == 0: 204 | return int(time.time()) 205 | 206 | df2 = pd.DataFrame(data) 207 | columns = ['timestamp'] 208 | df2 = df2.rename(columns={ 209 | 'startTime': 'timestamp', 210 | })[columns] 211 | df2['timestamp'] = pd.to_datetime(df2['timestamp'], utc=True) 212 | 213 | return df2['timestamp'].min().timestamp() 214 | 215 | def _find_total_end_time(self, market=None): 216 | try: 217 | future = self.ccxt_client.publicGetFuturesFutureName({ 218 | 'future_name': market, 219 | })['result'] 220 | except Exception as e: 221 | return time.time() - 1 222 | if future is not None and future['expiry'] is not None: 223 | return min([pd.to_datetime(future['expiry'], utc=True).timestamp(), time.time() - 1]) 224 | else: 225 | return time.time() - 1 226 | -------------------------------------------------------------------------------- /crypto_data_fetcher/gmo.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import pandas as pd 4 | import datetime 5 | import urllib.request 6 | from .utils import create_null_logger 7 | 8 | def url_exists(url): 9 | try: 10 | time.sleep(1) 11 | res = urllib.request.urlopen(url) 12 | if res.getcode() == 200: 13 | return True 14 | except urllib.error.HTTPError: 15 | pass 16 | return False 17 | 18 | def url_read_csv(url): 19 | try: 20 | time.sleep(1) 21 | df = pd.read_csv(url) 22 | df = df.rename(columns={ 23 | 'symbol': 'market', 24 | }) 25 | df['price'] = df['price'].astype('float64') 26 | df['size'] = df['size'].astype('float64') 27 | df['timestamp'] = pd.to_datetime(df['timestamp'], utc=True) 28 | df['side'] = np.where(df['side'] == 'BUY', 1, -1).astype('int8') 29 | return df 30 | except urllib.error.HTTPError: 31 | pass 32 | return None 33 | 34 | class GmoFetcher: 35 | def __init__(self, logger=None, ccxt_client=None, memory=None): 36 | self.logger = create_null_logger() if logger is None else logger 37 | self.ccxt_client = ccxt_client 38 | 39 | if memory is None: 40 | self._url_exists = url_exists 41 | self._url_read_csv = url_read_csv 42 | else: 43 | url_exists_cached = memory.cache(url_exists) 44 | self._url_exists = url_exists_cached 45 | 46 | url_read_csv_cached = memory.cache(url_read_csv) 47 | self._url_read_csv = url_read_csv_cached 48 | 49 | def fetch_ohlcv(self, interval_sec=None, market=None): 50 | return self.fetch_trades(market=market, interval_sec=interval_sec) 51 | 52 | def fetch_trades(self, market=None, interval_sec=None): 53 | if interval_sec is not None: 54 | if 3600 % interval_sec != 0: 55 | raise Exception('3600 % interval_sec must be 0') 56 | 57 | today = datetime.datetime.now().date() 58 | start_year, start_month = self._find_start_year_month(market) 59 | date = datetime.date(start_year, start_month, 1) 60 | 61 | dfs = [] 62 | while date < today: 63 | url = 'https://api.coin.z.com/data/trades/{}/{}/{:02}/{}{:02}{:02}_{}.csv.gz'.format( 64 | market, 65 | date.year, 66 | date.month, 67 | date.year, 68 | date.month, 69 | date.day, 70 | market, 71 | ) 72 | self.logger.debug(url) 73 | df = self._url_read_csv(url) 74 | 75 | if df is not None: 76 | if interval_sec is not None: 77 | df['timestamp'] = df['timestamp'].dt.floor('{}S'.format(interval_sec)) 78 | df = pd.concat([ 79 | df.groupby('timestamp')['price'].nth(0).rename('op'), 80 | df.groupby('timestamp')['price'].max().rename('hi'), 81 | df.groupby('timestamp')['price'].min().rename('lo'), 82 | df.groupby('timestamp')['price'].nth(-1).rename('cl'), 83 | df.groupby('timestamp')['size'].sum().rename('volume'), 84 | ], axis=1) 85 | 86 | dfs.append(df) 87 | 88 | date += datetime.timedelta(days=1) 89 | 90 | df = pd.concat(dfs) 91 | 92 | if interval_sec is None: 93 | df.reset_index(drop=True, inplace=True) 94 | 95 | return df 96 | 97 | def _find_start_year_month(self, market): 98 | today = datetime.datetime.now().date() 99 | 100 | for year in range(2018, today.year + 1): 101 | url = 'https://api.coin.z.com/data/trades/{}/{}/'.format(market, year) 102 | self.logger.debug(url) 103 | if self._url_exists(url): 104 | start_year = year 105 | break 106 | 107 | for month in range(1, 13): 108 | url = 'https://api.coin.z.com/data/trades/{}/{}/{:02}/'.format(market, start_year, month) 109 | self.logger.debug(url) 110 | if self._url_exists(url): 111 | start_month = month 112 | break 113 | 114 | return start_year, start_month 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /crypto_data_fetcher/kraken.py: -------------------------------------------------------------------------------- 1 | import time 2 | import math 3 | import pandas as pd 4 | from .utils import smart_append, create_null_logger, normalize_to_unix 5 | 6 | 7 | class KrakenFetcher: 8 | def __init__(self, logger=None, ccxt_client=None): 9 | self.logger = create_null_logger() if logger is None else logger 10 | self.ccxt_client = ccxt_client 11 | 12 | def fetch_ohlcv(self, df=None, start_time=None, interval_sec=None, market=None, price_type=None): 13 | if price_type is not None: 14 | raise Exception('price_type {} not implemented'.format(price_type)) 15 | 16 | if start_time: 17 | from_time = int(math.floor(normalize_to_unix(start_time))) 18 | else: 19 | from_time = 1 20 | 21 | if df is not None and df.shape[0]: 22 | from_time = int(math.floor(df.index.max().timestamp() + interval_sec)) 23 | 24 | dfs = [] 25 | 26 | end_time = int(math.floor(time.time())) 27 | 28 | while from_time < end_time: 29 | data = self.ccxt_client.publicGetOHLC({ 30 | 'pair': market, 31 | 'since': from_time - 1, # krakenはfrom_timeを含まない 32 | 'interval': interval_sec // 60, 33 | })['result'][market] 34 | 35 | if len(data) == 0: 36 | self.logger.debug('len(data) == 0') 37 | break 38 | 39 | df2 = pd.DataFrame(data, columns=[ 40 | 'timestamp', 41 | 'op', 42 | 'hi', 43 | 'lo', 44 | 'cl', 45 | 'vwap', 46 | 'volume', 47 | 'trade_count', 48 | ])[['timestamp', 'op', 'hi', 'lo', 'cl', 'volume']] 49 | 50 | df2 = df2[from_time <= df2['timestamp'].astype('int64')] 51 | if df2.shape[0] == 0: 52 | self.logger.debug('df2.shape[0] == 0') 53 | break 54 | 55 | df2['timestamp'] = pd.to_datetime(df2['timestamp'], unit='s', utc=True) 56 | 57 | for col in ['op', 'hi', 'lo', 'cl', 'volume']: 58 | df2[col] = df2[col].astype('float64') 59 | 60 | from_time = int(df2['timestamp'].max().timestamp()) + 1 61 | 62 | self.logger.debug('end_time {}'.format(end_time)) 63 | dfs.append(df2) 64 | 65 | if len(dfs) == 0: 66 | return None if df is None else df.copy() 67 | else: 68 | df = smart_append(df, pd.concat(dfs).set_index('timestamp')) 69 | # 最後は未確定足なので削除 70 | df = df[df.index != df.index.max()] 71 | return df 72 | 73 | -------------------------------------------------------------------------------- /crypto_data_fetcher/okex.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pandas as pd 3 | from .utils import smart_append, create_null_logger, normalize_to_unix 4 | 5 | class OkexFetcher: 6 | def __init__(self, logger=None, ccxt_client=None): 7 | self.logger = create_null_logger() if logger is None else logger 8 | self.ccxt_client = ccxt_client 9 | 10 | def fetch_ohlcv(self, df=None, start_time=None, interval_sec=None, market=None, price_type=None): 11 | if price_type is not None: 12 | raise Exception('price_type {} not implemented'.format(price_type)) 13 | 14 | if start_time: 15 | from_time = int(normalize_to_unix(start_time) * 1000) 16 | else: 17 | from_time = 1 18 | 19 | if df is not None and df.shape[0]: 20 | from_time = int((df.index.max().timestamp() + interval_sec) * 1000) 21 | 22 | dfs = [] 23 | 24 | end_time = int(time.time() * 1000 - 1) 25 | 26 | while from_time < end_time: 27 | data = self.ccxt_client.publicGetMarketHistoryCandles({ 28 | 'instId': market, 29 | 'after': end_time, # end_time未満 (ftxと違い、end_timeを含まない) 30 | 'bar': format_interval_sec(interval_sec), 31 | # limit: max is 100, default is 100 32 | })['data'] 33 | if len(data) == 0: 34 | self.logger.debug('len(data) == 0') 35 | break 36 | 37 | df2 = pd.DataFrame(data, columns=[ 38 | 'timestamp', 39 | 'op', 40 | 'hi', 41 | 'lo', 42 | 'cl', 43 | 'volume', 44 | 'quote_asset_volume', 45 | ])[['timestamp', 'op', 'hi', 'lo', 'cl', 'volume']] 46 | 47 | df2 = df2[from_time <= df2['timestamp'].astype('int64')] 48 | if df2.shape[0] == 0: 49 | self.logger.debug('df2.shape[0] == 0') 50 | break 51 | 52 | df2['timestamp'] = pd.to_datetime(df2['timestamp'], unit='ms', utc=True) 53 | 54 | for col in ['op', 'hi', 'lo', 'cl', 'volume']: 55 | df2[col] = df2[col].astype('float64') 56 | 57 | end_time = int(df2['timestamp'].min().timestamp() * 1000) 58 | self.logger.debug('end_time {}'.format(end_time)) 59 | dfs.append(df2) 60 | 61 | if len(dfs) == 0: 62 | return None if df is None else df.copy() 63 | else: 64 | df = smart_append(df, pd.concat(dfs).set_index('timestamp')) 65 | # okexは未確定足が返ってこないので削除不要 66 | return df 67 | 68 | # not implemented 69 | # def fetch_fr(self, df=None, start_time=None, market=None): 70 | # pass 71 | 72 | def format_interval_sec(interval_sec): 73 | interval_min = interval_sec // 60 74 | if interval_min < 60: 75 | return '{}m'.format(interval_min) 76 | if interval_min < 24 * 60: 77 | return '{}H'.format(interval_min // 60) 78 | else: 79 | return '{}D'.format(interval_min // (24 * 60)) 80 | -------------------------------------------------------------------------------- /crypto_data_fetcher/utils.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | 3 | def smart_append(df, other): 4 | if other is None or other.shape[0] == 0: 5 | return df.copy() 6 | if df is None: 7 | df = other.copy() 8 | else: 9 | df = df.append(other) 10 | df.sort_index(inplace=True, kind='mergesort') 11 | # https://stackoverflow.com/questions/13035764/remove-rows-with-duplicate-indices-pandas-dataframe-and-timeseries 12 | return df[~df.index.duplicated(keep='last')] 13 | 14 | def create_null_logger(): 15 | return getLogger(__name__ + 'null_logger') 16 | 17 | def normalize_to_unix(tm): 18 | if hasattr(tm, 'timestamp'): 19 | return tm.timestamp() 20 | else: 21 | return tm 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = crypto_data_fetcher 3 | version = 0.0.18 4 | author = richmanbtc 5 | description = crypto_data_fetcher 6 | long_description = file:README.md 7 | long_description_content_type=text/markdown 8 | url = https://github.com/richmanbtc/crypto_data_fetcher 9 | license = CC0 10 | classifier = 11 | Development Status :: 1 - Planning 12 | Programming Language :: Python 13 | Intended Audience :: Developers 14 | Operating System :: POSIX 15 | Programming Language :: Python :: 2 16 | Programming Language :: Python :: 2.7 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3.5 19 | Programming Language :: Python :: 3.6 20 | Programming Language :: Python :: 3.7 21 | 22 | [options] 23 | zip_safe = False 24 | packages = find: 25 | 26 | [options.packages.find] 27 | exclude = 28 | tests 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | setup() 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richmanbtc/crypto_data_fetcher/3471c8ea095f85bcd6c4d3207ce2d41af485a59d/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_binance_future.py: -------------------------------------------------------------------------------- 1 | import time 2 | import ccxt 3 | import pandas as pd 4 | from unittest import TestCase 5 | from crypto_data_fetcher.binance_future import BinanceFutureFetcher 6 | 7 | class TestBinanceFuture(TestCase): 8 | def test_fetch_ohlcv_initial(self): 9 | binance = ccxt.binance() 10 | fetcher = BinanceFutureFetcher(ccxt_client=binance) 11 | 12 | df = fetcher.fetch_ohlcv( 13 | market='BTCUSDT', 14 | interval_sec=24 * 60 * 60, 15 | ) 16 | 17 | self.assertEqual(df['op'].iloc[0], 10000.0) 18 | self.assertEqual(df['hi'].iloc[0], 10412.65) 19 | self.assertEqual(df['lo'].iloc[0], 10000.00) 20 | self.assertEqual(df['cl'].iloc[0], 10391.63) 21 | self.assertEqual(df['volume'].iloc[0], 3096.291) 22 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 23 | 24 | # 未確定足が無いことの確認 25 | self.assertEqual(df.index.max(), pd.to_datetime((time.time() // (24 * 60 * 60) - 1) * (24 * 60 * 60), unit='s', utc=True)) 26 | 27 | def test_fetch_ohlcv_start_time(self): 28 | binance = ccxt.binance() 29 | fetcher = BinanceFutureFetcher(ccxt_client=binance) 30 | 31 | df = fetcher.fetch_ohlcv( 32 | market='BTCUSDT', 33 | interval_sec=24 * 60 * 60, 34 | start_time=pd.to_datetime('2021-01-01 00:00:00Z', utc=True), 35 | ) 36 | 37 | self.assertEqual(df.index[0], pd.to_datetime('2021-01-01 00:00:00Z', utc=True)) 38 | 39 | def test_fetch_ohlcv_incremental(self): 40 | binance = ccxt.binance() 41 | fetcher = BinanceFutureFetcher(ccxt_client=binance) 42 | 43 | df = fetcher.fetch_ohlcv( 44 | market='BTCUSDT', 45 | interval_sec=24 * 60 * 60, 46 | ) 47 | last_row = df.iloc[-1] 48 | df = df.iloc[:-1] 49 | 50 | df = fetcher.fetch_ohlcv( 51 | df=df, 52 | market='BTCUSDT', 53 | interval_sec=24 * 60 * 60, 54 | ) 55 | self.assertTrue(df.iloc[-1].equals(last_row)) 56 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 57 | 58 | def test_fetch_ohlcv_incremental_empty(self): 59 | binance = ccxt.binance() 60 | fetcher = BinanceFutureFetcher(ccxt_client=binance) 61 | 62 | df = fetcher.fetch_ohlcv( 63 | market='BTCUSDT', 64 | interval_sec=24 * 60 * 60, 65 | ) 66 | before_count = df.shape[0] 67 | 68 | df = fetcher.fetch_ohlcv( 69 | df=df, 70 | market='BTCUSDT', 71 | interval_sec=24 * 60 * 60, 72 | ) 73 | self.assertEqual(df.shape[0], before_count) 74 | 75 | def test_fetch_ohlcv_initial_minute(self): 76 | binance = ccxt.binance() 77 | fetcher = BinanceFutureFetcher(ccxt_client=binance) 78 | 79 | df = fetcher.fetch_ohlcv( 80 | market='BTCUSDT', 81 | interval_sec=60, 82 | start_time=time.time() - 60 * 60 83 | ) 84 | 85 | self.assertGreater(df.shape[0], 1) 86 | self.assertLess(df.shape[0], 61) 87 | 88 | def test_fetch_ohlcv_out_of_range(self): 89 | binance = ccxt.binance() 90 | fetcher = BinanceFutureFetcher(ccxt_client=binance) 91 | 92 | df = fetcher.fetch_ohlcv( 93 | market='BTCUSDT', 94 | interval_sec=24 * 60 * 60, 95 | start_time=time.time() + 60 * 60 96 | ) 97 | 98 | self.assertIsNone(df) 99 | -------------------------------------------------------------------------------- /tests/test_binance_spot.py: -------------------------------------------------------------------------------- 1 | import time 2 | import ccxt 3 | import pandas as pd 4 | from unittest import TestCase 5 | from crypto_data_fetcher.binance_spot import BinanceSpotFetcher 6 | 7 | class TestBinanceSpot(TestCase): 8 | def test_fetch_ohlcv_initial(self): 9 | binance = ccxt.binance() 10 | fetcher = BinanceSpotFetcher(ccxt_client=binance) 11 | 12 | df = fetcher.fetch_ohlcv( 13 | market='BTCUSDT', 14 | interval_sec=24 * 60 * 60, 15 | ) 16 | 17 | self.assertEqual(df['op'].iloc[0], 4261.48) 18 | self.assertEqual(df['hi'].iloc[0], 4485.39) 19 | self.assertEqual(df['lo'].iloc[0], 4200.74) 20 | self.assertEqual(df['cl'].iloc[0], 4285.08) 21 | self.assertEqual(df['volume'].iloc[0], 795.150377) 22 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 23 | 24 | # 未確定足が無いことの確認 25 | self.assertEqual(df.index.max(), pd.to_datetime((time.time() // (24 * 60 * 60) - 1) * (24 * 60 * 60), unit='s', utc=True)) 26 | 27 | def test_fetch_ohlcv_start_time(self): 28 | binance = ccxt.binance() 29 | fetcher = BinanceSpotFetcher(ccxt_client=binance) 30 | 31 | df = fetcher.fetch_ohlcv( 32 | market='BTCUSDT', 33 | interval_sec=24 * 60 * 60, 34 | start_time=pd.to_datetime('2021-01-01 00:00:00Z', utc=True), 35 | ) 36 | 37 | self.assertEqual(df.index[0], pd.to_datetime('2021-01-01 00:00:00Z', utc=True)) 38 | 39 | def test_fetch_ohlcv_incremental(self): 40 | binance = ccxt.binance() 41 | fetcher = BinanceSpotFetcher(ccxt_client=binance) 42 | 43 | df = fetcher.fetch_ohlcv( 44 | market='BTCUSDT', 45 | interval_sec=24 * 60 * 60, 46 | ) 47 | last_row = df.iloc[-1] 48 | df = df.iloc[:-1] 49 | 50 | df = fetcher.fetch_ohlcv( 51 | df=df, 52 | market='BTCUSDT', 53 | interval_sec=24 * 60 * 60, 54 | ) 55 | self.assertTrue(df.iloc[-1].equals(last_row)) 56 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 57 | 58 | def test_fetch_ohlcv_incremental_empty(self): 59 | binance = ccxt.binance() 60 | fetcher = BinanceSpotFetcher(ccxt_client=binance) 61 | 62 | df = fetcher.fetch_ohlcv( 63 | market='BTCUSDT', 64 | interval_sec=24 * 60 * 60, 65 | ) 66 | before_count = df.shape[0] 67 | 68 | df = fetcher.fetch_ohlcv( 69 | df=df, 70 | market='BTCUSDT', 71 | interval_sec=24 * 60 * 60, 72 | ) 73 | self.assertEqual(df.shape[0], before_count) 74 | 75 | def test_fetch_ohlcv_initial_minute(self): 76 | binance = ccxt.binance() 77 | fetcher = BinanceSpotFetcher(ccxt_client=binance) 78 | 79 | df = fetcher.fetch_ohlcv( 80 | market='BTCUSDT', 81 | interval_sec=60, 82 | start_time=time.time() - 60 * 60 83 | ) 84 | 85 | self.assertGreater(df.shape[0], 1) 86 | self.assertLess(df.shape[0], 61) 87 | 88 | def test_fetch_ohlcv_out_of_range(self): 89 | binance = ccxt.binance() 90 | fetcher = BinanceSpotFetcher(ccxt_client=binance) 91 | 92 | df = fetcher.fetch_ohlcv( 93 | market='BTCUSDT', 94 | interval_sec=24 * 60 * 60, 95 | start_time=time.time() + 60 * 60 96 | ) 97 | 98 | self.assertIsNone(df) 99 | -------------------------------------------------------------------------------- /tests/test_bybit.py: -------------------------------------------------------------------------------- 1 | import time 2 | import ccxt 3 | import pandas as pd 4 | from unittest import TestCase 5 | from crypto_data_fetcher.bybit import BybitFetcher 6 | 7 | class TestBybit(TestCase): 8 | def test_fetch_ohlcv_initial(self): 9 | bybit = ccxt.bybit() 10 | fetcher = BybitFetcher(ccxt_client=bybit) 11 | 12 | df = fetcher.fetch_ohlcv( 13 | market='BTCUSD', 14 | interval_sec=24 * 60 * 60, 15 | ) 16 | 17 | self.assertEqual(df['op'].iloc[0], 5740) 18 | self.assertEqual(df['hi'].iloc[0], 5740) 19 | self.assertEqual(df['lo'].iloc[0], 5241) 20 | self.assertEqual(df['cl'].iloc[0], 5601.5) 21 | self.assertEqual(df['volume'].iloc[0], 14779268) 22 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 23 | 24 | # 未確定足が無いことの確認 25 | self.assertEqual(df.index.max(), pd.to_datetime((time.time() // (24 * 60 * 60) - 1) * (24 * 60 * 60), unit='s', utc=True)) 26 | 27 | def test_fetch_ohlcv_start_time(self): 28 | bybit = ccxt.bybit() 29 | fetcher = BybitFetcher(ccxt_client=bybit) 30 | 31 | df = fetcher.fetch_ohlcv( 32 | market='BTCUSD', 33 | interval_sec=24 * 60 * 60, 34 | start_time=pd.to_datetime('2021-01-01 00:00:00Z', utc=True), 35 | ) 36 | 37 | self.assertEqual(df.index[0], pd.to_datetime('2021-01-01 00:00:00Z', utc=True)) 38 | 39 | def test_fetch_ohlcv_incremental(self): 40 | bybit = ccxt.bybit() 41 | fetcher = BybitFetcher(ccxt_client=bybit) 42 | 43 | df = fetcher.fetch_ohlcv( 44 | market='BTCUSD', 45 | interval_sec=24 * 60 * 60, 46 | ) 47 | last_row = df.iloc[-1] 48 | df = df.iloc[:-1] 49 | 50 | df = fetcher.fetch_ohlcv( 51 | df=df, 52 | market='BTCUSD', 53 | interval_sec=24 * 60 * 60, 54 | ) 55 | self.assertTrue(df.iloc[-1].equals(last_row)) 56 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 57 | 58 | def test_fetch_ohlcv_incremental_empty(self): 59 | bybit = ccxt.bybit() 60 | fetcher = BybitFetcher(ccxt_client=bybit) 61 | 62 | df = fetcher.fetch_ohlcv( 63 | market='BTCUSD', 64 | interval_sec=24 * 60 * 60, 65 | ) 66 | before_count = df.shape[0] 67 | 68 | df = fetcher.fetch_ohlcv( 69 | df=df, 70 | market='BTCUSD', 71 | interval_sec=24 * 60 * 60, 72 | ) 73 | self.assertEqual(df.shape[0], before_count) 74 | 75 | def test_fetch_ohlcv_mark(self): 76 | bybit = ccxt.bybit() 77 | fetcher = BybitFetcher(ccxt_client=bybit) 78 | 79 | df = fetcher.fetch_ohlcv( 80 | market='BTCUSD', 81 | interval_sec=24 * 60 * 60, 82 | price_type='mark', 83 | ) 84 | 85 | self.assertEqual(df['op'].iloc[0], 6145.56982421875) 86 | self.assertEqual(df['hi'].iloc[0], 6145.56982421875) 87 | self.assertEqual(df['lo'].iloc[0], 5324.22998046875) 88 | self.assertEqual(df['cl'].iloc[0], 5595.58984375) 89 | 90 | # 2個欠損しているらしい (より細かい足はあるから、bybitの集約の仕様だと思う) 91 | # 7 2018-11-22 00:00:00+00:00 4487.040039 4487.040039 4487.040039 4487.040039 2 days 92 | # 249 2019-07-23 00:00:00+00:00 93 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1 + 2) * 24 * 60 * 60) 94 | 95 | def test_fetch_ohlcv_index(self): 96 | bybit = ccxt.bybit() 97 | fetcher = BybitFetcher(ccxt_client=bybit) 98 | 99 | df = fetcher.fetch_ohlcv( 100 | market='BTCUSD', 101 | interval_sec=24 * 60 * 60, 102 | price_type='index', 103 | ) 104 | 105 | self.assertEqual(df['op'].iloc[0], 3872.41) 106 | self.assertEqual(df['hi'].iloc[0], 3878.63) 107 | self.assertEqual(df['lo'].iloc[0], 3816.38) 108 | self.assertEqual(df['cl'].iloc[0], 3850.19) 109 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 110 | 111 | def test_fetch_ohlcv_premium_index(self): 112 | bybit = ccxt.bybit() 113 | fetcher = BybitFetcher(ccxt_client=bybit) 114 | 115 | df = fetcher.fetch_ohlcv( 116 | market='BTCUSD', 117 | interval_sec=24 * 60 * 60, 118 | price_type='premium_index', 119 | ) 120 | self.assertEqual(df['op'].iloc[0], -0.000346) 121 | self.assertEqual(df['hi'].iloc[0], 0.002467) 122 | self.assertEqual(df['lo'].iloc[0], -0.009693) 123 | self.assertEqual(df['cl'].iloc[0], -0.000026) 124 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 125 | 126 | def test_calc_fr_from_premium_index(self): 127 | bybit = ccxt.bybit() 128 | fetcher = BybitFetcher(ccxt_client=bybit) 129 | 130 | df_premium_index = fetcher.fetch_ohlcv( 131 | market='BTCUSD', 132 | interval_sec=24 * 60 * 60, 133 | price_type='premium_index', 134 | ) 135 | df = fetcher.calc_fr_from_premium_index(df_premium_index=df_premium_index) 136 | print(df) 137 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 138 | 139 | def test_fetch_ohlcv_initial_minute(self): 140 | bybit = ccxt.bybit() 141 | fetcher = BybitFetcher(ccxt_client=bybit) 142 | 143 | df = fetcher.fetch_ohlcv( 144 | market='BTCUSD', 145 | interval_sec=60, 146 | start_time=time.time() - 60 * 60 147 | ) 148 | 149 | self.assertGreater(df.shape[0], 1) 150 | self.assertLess(df.shape[0], 61) 151 | 152 | def test_fetch_ohlcv_out_of_range(self): 153 | bybit = ccxt.bybit() 154 | fetcher = BybitFetcher(ccxt_client=bybit) 155 | 156 | df = fetcher.fetch_ohlcv( 157 | market='BTCUSD', 158 | interval_sec=24 * 60 * 60, 159 | start_time=time.time() + 60 * 60 160 | ) 161 | 162 | self.assertIsNone(df) 163 | -------------------------------------------------------------------------------- /tests/test_bybit_linear.py: -------------------------------------------------------------------------------- 1 | import time 2 | import ccxt 3 | import pandas as pd 4 | from unittest import TestCase 5 | from crypto_data_fetcher.bybit import BybitFetcher 6 | 7 | class TestBybitLinear(TestCase): 8 | def test_fetch_ohlcv_initial(self): 9 | bybit = ccxt.bybit() 10 | fetcher = BybitFetcher(ccxt_client=bybit) 11 | 12 | df = fetcher.fetch_ohlcv( 13 | market='BTCUSDT', 14 | interval_sec=24 * 60 * 60, 15 | ) 16 | 17 | self.assertEqual(df['op'].iloc[0], 6500) 18 | self.assertEqual(df['hi'].iloc[0], 6745.5) 19 | self.assertEqual(df['lo'].iloc[0], 6500) 20 | self.assertEqual(df['cl'].iloc[0], 6698.5) 21 | self.assertEqual(df['volume'].iloc[0], 1809.520) 22 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 23 | 24 | # 未確定足が無いことの確認 25 | self.assertEqual(df.index.max(), pd.to_datetime((time.time() // (24 * 60 * 60) - 1) * (24 * 60 * 60), unit='s', utc=True)) 26 | 27 | def test_fetch_ohlcv_start_time(self): 28 | bybit = ccxt.bybit() 29 | fetcher = BybitFetcher(ccxt_client=bybit) 30 | 31 | df = fetcher.fetch_ohlcv( 32 | market='BTCUSDT', 33 | interval_sec=24 * 60 * 60, 34 | start_time=pd.to_datetime('2021-01-01 00:00:00Z', utc=True), 35 | ) 36 | 37 | self.assertEqual(df.index[0], pd.to_datetime('2021-01-01 00:00:00Z', utc=True)) 38 | 39 | def test_fetch_ohlcv_incremental(self): 40 | bybit = ccxt.bybit() 41 | fetcher = BybitFetcher(ccxt_client=bybit) 42 | 43 | df = fetcher.fetch_ohlcv( 44 | market='BTCUSDT', 45 | interval_sec=24 * 60 * 60, 46 | ) 47 | last_row = df.iloc[-1] 48 | df = df.iloc[:-1] 49 | 50 | df = fetcher.fetch_ohlcv( 51 | df=df, 52 | market='BTCUSDT', 53 | interval_sec=24 * 60 * 60, 54 | ) 55 | self.assertTrue(df.iloc[-1].equals(last_row)) 56 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 57 | 58 | def test_fetch_ohlcv_incremental_empty(self): 59 | bybit = ccxt.bybit() 60 | fetcher = BybitFetcher(ccxt_client=bybit) 61 | 62 | df = fetcher.fetch_ohlcv( 63 | market='BTCUSDT', 64 | interval_sec=24 * 60 * 60, 65 | ) 66 | before_count = df.shape[0] 67 | 68 | df = fetcher.fetch_ohlcv( 69 | df=df, 70 | market='BTCUSDT', 71 | interval_sec=24 * 60 * 60, 72 | ) 73 | self.assertEqual(df.shape[0], before_count) 74 | 75 | def test_fetch_ohlcv_mark(self): 76 | bybit = ccxt.bybit() 77 | fetcher = BybitFetcher(ccxt_client=bybit) 78 | 79 | df = fetcher.fetch_ohlcv( 80 | market='BTCUSDT', 81 | interval_sec=24 * 60 * 60, 82 | price_type='mark', 83 | ) 84 | print(df) 85 | 86 | self.assertEqual(df['op'].iloc[0], 6718.21) 87 | self.assertEqual(df['hi'].iloc[0], 6955.88) 88 | self.assertEqual(df['lo'].iloc[0], 6451.31) 89 | self.assertEqual(df['cl'].iloc[0], 6677.77) 90 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 91 | 92 | def test_fetch_ohlcv_index(self): 93 | bybit = ccxt.bybit() 94 | fetcher = BybitFetcher(ccxt_client=bybit) 95 | 96 | df = fetcher.fetch_ohlcv( 97 | market='BTCUSDT', 98 | interval_sec=24 * 60 * 60, 99 | price_type='index', 100 | ) 101 | print(df) 102 | 103 | self.assertEqual(df['op'].iloc[0], 6670.01) 104 | self.assertEqual(df['hi'].iloc[0], 6956.84) 105 | self.assertEqual(df['lo'].iloc[0], 6450.96) 106 | self.assertEqual(df['cl'].iloc[0], 6677.77) 107 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 108 | 109 | def test_fetch_ohlcv_premium_index(self): 110 | bybit = ccxt.bybit() 111 | fetcher = BybitFetcher(ccxt_client=bybit) 112 | 113 | df = fetcher.fetch_ohlcv( 114 | market='BTCUSDT', 115 | interval_sec=24 * 60 * 60, 116 | price_type='premium_index', 117 | ) 118 | print(df) 119 | 120 | self.assertEqual(df['op'].iloc[0], 0.000100) 121 | self.assertEqual(df['hi'].iloc[0], 0.000360) 122 | self.assertEqual(df['lo'].iloc[0], -0.000865) 123 | self.assertEqual(df['cl'].iloc[0], -0.000097) 124 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 125 | 126 | def test_calc_fr_from_premium_index(self): 127 | bybit = ccxt.bybit() 128 | fetcher = BybitFetcher(ccxt_client=bybit) 129 | 130 | df_premium_index = fetcher.fetch_ohlcv( 131 | market='BTCUSDT', 132 | interval_sec=24 * 60 * 60, 133 | price_type='premium_index', 134 | ) 135 | df = fetcher.calc_fr_from_premium_index(df_premium_index=df_premium_index) 136 | print(df) 137 | 138 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 139 | 140 | def test_fetch_ohlcv_initial_minute(self): 141 | bybit = ccxt.bybit() 142 | fetcher = BybitFetcher(ccxt_client=bybit) 143 | 144 | df = fetcher.fetch_ohlcv( 145 | market='BTCUSDT', 146 | interval_sec=60, 147 | start_time=time.time() - 60 * 60 148 | ) 149 | 150 | self.assertGreater(df.shape[0], 1) 151 | self.assertLess(df.shape[0], 61) 152 | 153 | def test_fetch_ohlcv_out_of_range(self): 154 | bybit = ccxt.bybit() 155 | fetcher = BybitFetcher(ccxt_client=bybit) 156 | 157 | df = fetcher.fetch_ohlcv( 158 | market='BTCUSDT', 159 | interval_sec=24 * 60 * 60, 160 | start_time=time.time() + 60 * 60 161 | ) 162 | 163 | self.assertIsNone(df) 164 | -------------------------------------------------------------------------------- /tests/test_ftx.py: -------------------------------------------------------------------------------- 1 | import time 2 | import ccxt 3 | import pandas as pd 4 | from unittest import TestCase, mock 5 | from crypto_data_fetcher.ftx import FtxFetcher 6 | 7 | class TestFtx(TestCase): 8 | def test_fetch_ohlcv_initial(self): 9 | ftx = ccxt.ftx() 10 | fetcher = FtxFetcher(ccxt_client=ftx) 11 | 12 | df = fetcher.fetch_ohlcv( 13 | market='BTC-PERP', 14 | interval_sec=24 * 60 * 60, 15 | ) 16 | print(df) 17 | 18 | self.assertEqual(df['op'].iloc[0], 10564.25) 19 | self.assertEqual(df['hi'].iloc[0], 11108.75) 20 | self.assertEqual(df['lo'].iloc[0], 10385.25) 21 | self.assertEqual(df['cl'].iloc[0], 10765.75) 22 | self.assertEqual(df['volume'].iloc[0], 117203195.32235) 23 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 24 | 25 | # 未確定足が無いことの確認 26 | self.assertEqual(df.index.max(), pd.to_datetime((time.time() // (24 * 60 * 60) - 1) * (24 * 60 * 60), unit='s', utc=True)) 27 | 28 | def test_fetch_ohlcv_index(self): 29 | ftx = ccxt.ftx() 30 | fetcher = FtxFetcher(ccxt_client=ftx) 31 | 32 | df = fetcher.fetch_ohlcv( 33 | market='BTC-PERP', 34 | interval_sec=24 * 60 * 60, 35 | price_type='index', 36 | ) 37 | print(df) 38 | 39 | self.assertEqual(df['op'].iloc[0], 10532.400561321) 40 | self.assertEqual(df['hi'].iloc[0], 11094.361620957) 41 | self.assertEqual(df['lo'].iloc[0], 10385.558948835) 42 | self.assertEqual(df['cl'].iloc[0], 10758.259167146) 43 | self.assertTrue('volume' not in df.columns) 44 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 45 | 46 | # 未確定足が無いことの確認 47 | self.assertEqual(df.index.max(), pd.to_datetime((time.time() // (24 * 60 * 60) - 1) * (24 * 60 * 60), unit='s', utc=True)) 48 | 49 | def test_fetch_ohlcv_start_time(self): 50 | ftx = ccxt.ftx() 51 | fetcher = FtxFetcher(ccxt_client=ftx) 52 | 53 | df = fetcher.fetch_ohlcv( 54 | market='BTC-PERP', 55 | interval_sec=24 * 60 * 60, 56 | start_time=pd.to_datetime('2021-01-01 00:00:00Z', utc=True), 57 | ) 58 | 59 | self.assertEqual(df.index[0], pd.to_datetime('2021-01-01 00:00:00Z', utc=True)) 60 | 61 | def test_fetch_ohlcv_incremental(self): 62 | ftx = ccxt.ftx() 63 | fetcher = FtxFetcher(ccxt_client=ftx) 64 | 65 | df = fetcher.fetch_ohlcv( 66 | market='BTC-PERP', 67 | interval_sec=24 * 60 * 60, 68 | ) 69 | last_row = df.iloc[-1] 70 | df = df.iloc[:-1] 71 | 72 | df = fetcher.fetch_ohlcv( 73 | df=df, 74 | market='BTC-PERP', 75 | interval_sec=24 * 60 * 60, 76 | ) 77 | self.assertTrue(df.iloc[-1].equals(last_row)) 78 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 79 | 80 | def test_fetch_ohlcv_incremental_empty(self): 81 | ftx = ccxt.ftx() 82 | fetcher = FtxFetcher(ccxt_client=ftx) 83 | 84 | df = fetcher.fetch_ohlcv( 85 | market='BTC-PERP', 86 | interval_sec=24 * 60 * 60, 87 | ) 88 | before_count = df.shape[0] 89 | 90 | df = fetcher.fetch_ohlcv( 91 | df=df, 92 | market='BTC-PERP', 93 | interval_sec=24 * 60 * 60, 94 | ) 95 | self.assertEqual(df.shape[0], before_count) 96 | 97 | def test_fetch_fr_initial(self): 98 | ftx = ccxt.ftx() 99 | fetcher = FtxFetcher(ccxt_client=ftx) 100 | 101 | df = fetcher.fetch_fr( 102 | market='BTC-PERP', 103 | ) 104 | 105 | self.assertEqual(df['fr'].iloc[0], 1e-05) 106 | # 4つ欠損があるらしい 107 | # 146 2019-07-26 04:00:00+00:00 0.000013 0 days 03:00:00 108 | # 2351 2019-10-26 03:00:00+00:00 -0.001415 0 days 03:00:00 109 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1 + 4) * 60 * 60) 110 | 111 | # 現在足まで取れていることの確認 112 | self.assertEqual(df.index.max(), pd.to_datetime(time.time() // (60 * 60) * (60 * 60), unit='s', utc=True)) 113 | 114 | def test_fetch_fr_start_time(self): 115 | ftx = ccxt.ftx() 116 | fetcher = FtxFetcher(ccxt_client=ftx) 117 | 118 | df = fetcher.fetch_fr( 119 | market='BTC-PERP', 120 | start_time=pd.to_datetime('2021-01-01 00:00:00Z', utc=True), 121 | ) 122 | 123 | self.assertEqual(df.index[0], pd.to_datetime('2021-01-01 00:00:00Z', utc=True)) 124 | 125 | def test_fetch_fr_incremental(self): 126 | ftx = ccxt.ftx() 127 | fetcher = FtxFetcher(ccxt_client=ftx) 128 | 129 | df = fetcher.fetch_fr( 130 | market='BTC-PERP', 131 | ) 132 | last_row = df.iloc[-1] 133 | df = df.iloc[:-1] 134 | 135 | df = fetcher.fetch_fr( 136 | df=df, 137 | market='BTC-PERP', 138 | ) 139 | self.assertTrue(df.iloc[-1].equals(last_row)) 140 | # 4つ欠損があるらしい 141 | # 146 2019-07-26 04:00:00+00:00 0.000013 0 days 03:00:00 142 | # 2351 2019-10-26 03:00:00+00:00 -0.001415 0 days 03:00:00 143 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1 + 4) * 60 * 60) 144 | 145 | def test_fetch_fr_incremental_empty(self): 146 | ftx = ccxt.ftx() 147 | fetcher = FtxFetcher(ccxt_client=ftx) 148 | 149 | df = fetcher.fetch_fr( 150 | market='BTC-PERP', 151 | ) 152 | before_count = df.shape[0] 153 | 154 | df = fetcher.fetch_fr( 155 | df=df, 156 | market='BTC-PERP', 157 | ) 158 | self.assertEqual(df.shape[0], before_count) 159 | 160 | def test_fetch_ohlcv_initial_minute(self): 161 | ftx = ccxt.ftx() 162 | fetcher = FtxFetcher(ccxt_client=ftx) 163 | 164 | df = fetcher.fetch_ohlcv( 165 | market='BTC-PERP', 166 | interval_sec=60, 167 | start_time=time.time() - 60 * 60 168 | ) 169 | 170 | self.assertGreater(df.shape[0], 1) 171 | self.assertLess(df.shape[0], 61) 172 | 173 | def test_fetch_ohlcv_out_of_range(self): 174 | ftx = ccxt.ftx() 175 | fetcher = FtxFetcher(ccxt_client=ftx) 176 | 177 | df = fetcher.fetch_ohlcv( 178 | market='BTC-20201225', 179 | interval_sec=24 * 60 * 60, 180 | start_time=time.time() - 7 * 24 * 60 * 60 181 | ) 182 | 183 | self.assertIsNone(df) 184 | 185 | def test_fetch_fr_out_of_range(self): 186 | ftx = ccxt.ftx() 187 | fetcher = FtxFetcher(ccxt_client=ftx) 188 | 189 | df = fetcher.fetch_fr( 190 | market='BTC-20201225', 191 | start_time=time.time() - 7 * 24 * 60 * 60 192 | ) 193 | 194 | self.assertIsNone(df) 195 | 196 | def test_fetch_fr_old_future(self): 197 | ftx = ccxt.ftx() 198 | fetcher = FtxFetcher(ccxt_client=ftx) 199 | 200 | df = fetcher.fetch_fr( 201 | market='BNB-20190329', 202 | ) 203 | 204 | self.assertIsNone(df) 205 | 206 | def test_find_total_end_time(self): 207 | ftx = ccxt.ftx() 208 | fetcher = FtxFetcher(ccxt_client=ftx) 209 | 210 | end_time = fetcher._find_total_end_time( 211 | market='BTC-20201225', 212 | ) 213 | 214 | self.assertEqual(end_time, 1608865200) 215 | 216 | @mock.patch('time.time', mock.MagicMock(return_value=12345)) 217 | def test_find_total_end_time_spot(self): 218 | ftx = ccxt.ftx() 219 | fetcher = FtxFetcher(ccxt_client=ftx) 220 | 221 | end_time = fetcher._find_total_end_time( 222 | market='BTC/USD', 223 | ) 224 | 225 | self.assertEqual(end_time, 12345 - 1) 226 | 227 | @mock.patch('time.time', mock.MagicMock(return_value=12345)) 228 | def test_find_total_end_time_perp(self): 229 | ftx = ccxt.ftx() 230 | fetcher = FtxFetcher(ccxt_client=ftx) 231 | 232 | end_time = fetcher._find_total_end_time( 233 | market='BTC-PERP', 234 | ) 235 | 236 | self.assertEqual(end_time, 12345 - 1) 237 | 238 | def test_fetch_ohlcv_old_future(self): 239 | ftx = ccxt.ftx() 240 | fetcher = FtxFetcher(ccxt_client=ftx) 241 | 242 | df = fetcher.fetch_ohlcv( 243 | market='BTC-20201225', 244 | interval_sec=24 * 60 * 60, 245 | ) 246 | 247 | self.assertEqual(df.index[0], pd.to_datetime('2020-06-08 00:00:00Z', utc=True)) 248 | 249 | # 後でなおす 250 | # def test_fetch_ohlcv_bug(self): 251 | # ftx = ccxt.ftx() 252 | # fetcher = FtxFetcher(ccxt_client=ftx) 253 | # 254 | # print(fetcher._find_total_end_time( 255 | # market='MKR-20200626', 256 | # )) 257 | # 258 | # df = fetcher.fetch_ohlcv( 259 | # market='MKR-20200626', 260 | # interval_sec=300, 261 | # ) 262 | # 263 | # self.assertEqual(df.index[0], pd.to_datetime('2020-06-08 00:00:00Z', utc=True)) 264 | -------------------------------------------------------------------------------- /tests/test_ftx_private.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | import ccxt 5 | import pandas as pd 6 | from unittest import TestCase, mock 7 | from crypto_data_fetcher.ftx import FtxFetcher 8 | 9 | def ftx_config(): 10 | path = os.getenv("HOME") + '/.ftx.json' 11 | with open(path) as f: 12 | return json.load(f) 13 | 14 | def create_ccxt_client(): 15 | headers = { 16 | 'FTX-SUBACCOUNT': 'bottest' 17 | } 18 | 19 | return ccxt.ftx({ 20 | 'apiKey': ftx_config()['key'], 21 | 'secret': ftx_config()['secret'], 22 | 'enableRateLimit': True, 23 | 'headers': headers, 24 | }) 25 | 26 | # class TestFtxPrivate(TestCase): 27 | # def test_fetch_my_trades(self): 28 | # ftx = create_ccxt_client() 29 | # fetcher = FtxFetcher(ccxt_client=ftx) 30 | # 31 | # df = fetcher.fetch_my_trades( 32 | # market='BTC-PERP', 33 | # ) 34 | # 35 | # print(df) 36 | -------------------------------------------------------------------------------- /tests/test_gmo.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import logging 3 | import sys 4 | from unittest import TestCase, mock 5 | from crypto_data_fetcher.gmo import GmoFetcher 6 | from joblib import Memory 7 | 8 | memory_location = '/tmp/crypto_data_fetcher_test_gmo' 9 | 10 | class TestGmo(TestCase): 11 | def test_fetch_ohlcv(self): 12 | logger = logging.getLogger(__name__) 13 | logger.setLevel(logging.DEBUG) 14 | 15 | stderr_handler = logging.StreamHandler(stream=sys.stderr) 16 | stderr_handler.setLevel(logging.DEBUG) 17 | logger.addHandler(stderr_handler) 18 | 19 | memory = Memory(memory_location, verbose=1) 20 | fetcher = GmoFetcher(logger=logger, memory=memory) 21 | 22 | df = fetcher.fetch_ohlcv(market='XLM', interval_sec=300) 23 | print(df) 24 | print(df.dtypes) 25 | 26 | self.assertEqual(df.index[0], pd.to_datetime('2021-08-18 07:10:00+00:00', utc=True)) 27 | 28 | def test_fetch_trades(self): 29 | logger = logging.getLogger(__name__) 30 | logger.setLevel(logging.DEBUG) 31 | 32 | stderr_handler = logging.StreamHandler(stream=sys.stderr) 33 | stderr_handler.setLevel(logging.DEBUG) 34 | logger.addHandler(stderr_handler) 35 | 36 | memory = Memory(memory_location, verbose=1) 37 | fetcher = GmoFetcher(logger=logger, memory=memory) 38 | 39 | df = fetcher.fetch_trades(market='XLM') 40 | print(df) 41 | print(df.dtypes) 42 | 43 | self.assertEqual(df['timestamp'].iloc[0], pd.to_datetime('2021-08-18 07:10:38.186000+00:00', utc=True)) 44 | -------------------------------------------------------------------------------- /tests/test_kraken.py: -------------------------------------------------------------------------------- 1 | import time 2 | import ccxt 3 | import pandas as pd 4 | from unittest import TestCase 5 | from crypto_data_fetcher.kraken import KrakenFetcher 6 | 7 | market = 'XXBTZUSD' 8 | 9 | class TestKraken(TestCase): 10 | def test_fetch_ohlcv_initial(self): 11 | kraken = ccxt.kraken() 12 | fetcher = KrakenFetcher(ccxt_client=kraken) 13 | 14 | df = fetcher.fetch_ohlcv( 15 | market=market, 16 | interval_sec=24 * 60 * 60, 17 | ) 18 | 19 | self.assertEqual(df['op'].iloc[0], 8526.10000) 20 | self.assertEqual(df['hi'].iloc[0], 8750.00000) 21 | self.assertEqual(df['lo'].iloc[0], 8405.00000) 22 | self.assertEqual(df['cl'].iloc[0], 8530.00000) 23 | self.assertEqual(df['volume'].iloc[0], 3134.04788956) 24 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 25 | 26 | # 未確定足が無いことの確認 27 | self.assertEqual(df.index.max(), pd.to_datetime((time.time() // (24 * 60 * 60) - 1) * (24 * 60 * 60), unit='s', utc=True)) 28 | 29 | def test_fetch_ohlcv_start_time(self): 30 | kraken = ccxt.kraken() 31 | fetcher = KrakenFetcher(ccxt_client=kraken) 32 | 33 | df = fetcher.fetch_ohlcv( 34 | market=market, 35 | interval_sec=24 * 60 * 60, 36 | start_time=pd.to_datetime('2021-01-01 00:00:00Z', utc=True), 37 | ) 38 | 39 | self.assertEqual(df.index[0], pd.to_datetime('2021-01-01 00:00:00Z', utc=True)) 40 | 41 | def test_fetch_ohlcv_incremental(self): 42 | kraken = ccxt.kraken() 43 | fetcher = KrakenFetcher(ccxt_client=kraken) 44 | 45 | df = fetcher.fetch_ohlcv( 46 | market=market, 47 | interval_sec=24 * 60 * 60, 48 | ) 49 | last_row = df.iloc[-1] 50 | df = df.iloc[:-1] 51 | 52 | df = fetcher.fetch_ohlcv( 53 | df=df, 54 | market=market, 55 | interval_sec=24 * 60 * 60, 56 | ) 57 | self.assertTrue(df.iloc[-1].equals(last_row)) 58 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 59 | 60 | def test_fetch_ohlcv_incremental_empty(self): 61 | kraken = ccxt.kraken() 62 | fetcher = KrakenFetcher(ccxt_client=kraken) 63 | 64 | df = fetcher.fetch_ohlcv( 65 | market=market, 66 | interval_sec=24 * 60 * 60, 67 | ) 68 | before_count = df.shape[0] 69 | 70 | df = fetcher.fetch_ohlcv( 71 | df=df, 72 | market=market, 73 | interval_sec=24 * 60 * 60, 74 | ) 75 | self.assertEqual(df.shape[0], before_count) 76 | 77 | def test_fetch_ohlcv_initial_minute(self): 78 | kraken = ccxt.kraken() 79 | fetcher = KrakenFetcher(ccxt_client=kraken) 80 | 81 | df = fetcher.fetch_ohlcv( 82 | market=market, 83 | interval_sec=60, 84 | start_time=time.time() - 60 * 60 85 | ) 86 | 87 | self.assertGreater(df.shape[0], 1) 88 | self.assertLess(df.shape[0], 61) 89 | 90 | def test_fetch_ohlcv_out_of_range(self): 91 | kraken = ccxt.kraken() 92 | fetcher = KrakenFetcher(ccxt_client=kraken) 93 | 94 | df = fetcher.fetch_ohlcv( 95 | market=market, 96 | interval_sec=24 * 60 * 60, 97 | start_time=time.time() + 60 * 60 98 | ) 99 | 100 | self.assertIsNone(df) 101 | -------------------------------------------------------------------------------- /tests/test_okex.py: -------------------------------------------------------------------------------- 1 | import time 2 | import ccxt 3 | import logging 4 | import sys 5 | import pandas as pd 6 | from unittest import TestCase 7 | from crypto_data_fetcher.okex import OkexFetcher 8 | 9 | logger = logging.getLogger(__name__) 10 | logger.setLevel(level=logging.DEBUG) 11 | 12 | handler = logging.StreamHandler(sys.stderr) 13 | handler.setLevel(logging.DEBUG) 14 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 15 | handler.setFormatter(formatter) 16 | logger.addHandler(handler) 17 | 18 | market = 'BTC-USDT-SWAP' 19 | 20 | class TestOkex(TestCase): 21 | def test_fetch_ohlcv_initial(self): 22 | okex = ccxt.okex() 23 | fetcher = OkexFetcher(ccxt_client=okex, logger=logger) 24 | 25 | df = fetcher.fetch_ohlcv( 26 | market=market, 27 | interval_sec=24 * 60 * 60, 28 | ) 29 | 30 | self.assertEqual(df['op'].iloc[0], 7500.0) 31 | self.assertEqual(df['hi'].iloc[0], 7652.7) 32 | self.assertEqual(df['lo'].iloc[0], 7315.5) 33 | self.assertEqual(df['cl'].iloc[0], 7350.0) 34 | self.assertEqual(df['volume'].iloc[0], 3110.0) 35 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 36 | 37 | # 未確定足が無いことの確認 38 | shift = 16 * 60 * 60 # okexの日足は16:00 39 | self.assertEqual(df.index.max(), pd.to_datetime(((time.time() - shift) // (24 * 60 * 60) - 1) * (24 * 60 * 60) + shift, unit='s', utc=True)) 40 | 41 | def test_fetch_ohlcv_start_time(self): 42 | okex = ccxt.okex() 43 | fetcher = OkexFetcher(ccxt_client=okex) 44 | 45 | df = fetcher.fetch_ohlcv( 46 | market=market, 47 | interval_sec=24 * 60 * 60, 48 | start_time=pd.to_datetime('2021-01-01 16:00:00Z', utc=True), # okexの日足は16:00らしい 49 | ) 50 | 51 | self.assertEqual(df.index[0], pd.to_datetime('2021-01-01 16:00:00Z', utc=True)) 52 | 53 | def test_fetch_ohlcv_incremental(self): 54 | okex = ccxt.okex() 55 | fetcher = OkexFetcher(ccxt_client=okex) 56 | 57 | df = fetcher.fetch_ohlcv( 58 | market=market, 59 | interval_sec=24 * 60 * 60, 60 | ) 61 | last_row = df.iloc[-1] 62 | df = df.iloc[:-1] 63 | 64 | df = fetcher.fetch_ohlcv( 65 | df=df, 66 | market=market, 67 | interval_sec=24 * 60 * 60, 68 | ) 69 | self.assertTrue(df.iloc[-1].equals(last_row)) 70 | self.assertEqual(df.index[-1].timestamp() - df.index[0].timestamp(), (df.shape[0] - 1) * 24 * 60 * 60) 71 | 72 | def test_fetch_ohlcv_incremental_empty(self): 73 | okex = ccxt.okex() 74 | fetcher = OkexFetcher(ccxt_client=okex) 75 | 76 | df = fetcher.fetch_ohlcv( 77 | market=market, 78 | interval_sec=24 * 60 * 60, 79 | ) 80 | before_count = df.shape[0] 81 | 82 | df = fetcher.fetch_ohlcv( 83 | df=df, 84 | market=market, 85 | interval_sec=24 * 60 * 60, 86 | ) 87 | self.assertEqual(df.shape[0], before_count) 88 | 89 | def test_fetch_ohlcv_initial_minute(self): 90 | okex = ccxt.okex() 91 | fetcher = OkexFetcher(ccxt_client=okex) 92 | 93 | df = fetcher.fetch_ohlcv( 94 | market=market, 95 | interval_sec=60, 96 | start_time=time.time() - 60 * 60 97 | ) 98 | 99 | self.assertGreater(df.shape[0], 1) 100 | self.assertLess(df.shape[0], 61) 101 | 102 | def test_fetch_ohlcv_out_of_range(self): 103 | okex = ccxt.okex() 104 | fetcher = OkexFetcher(ccxt_client=okex) 105 | 106 | df = fetcher.fetch_ohlcv( 107 | market=market, 108 | interval_sec=24 * 60 * 60, 109 | start_time=time.time() + 60 * 60 110 | ) 111 | 112 | self.assertIsNone(df) 113 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from unittest import TestCase 3 | from crypto_data_fetcher.utils import normalize_to_unix 4 | 5 | class TestUtils(TestCase): 6 | def test_normalize_to_unix(self): 7 | self.assertEqual(normalize_to_unix(1), 1) 8 | self.assertEqual(normalize_to_unix(pd.to_datetime(1, unit='s', utc=True)), 1) 9 | 10 | --------------------------------------------------------------------------------