├── .github └── workflows │ └── container.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── create-graphs.py ├── docker-compose.yaml ├── graphs └── .gitkeep ├── models ├── Binance.py ├── CoinbasePro.py ├── PyCryptoBot.py ├── Telegram.py ├── Trading.py ├── TradingAccount.py └── __init__.py ├── pycryptobot.py ├── pytest.ini ├── requirements.txt ├── sandbox-tracker.py ├── script-get_orders.py ├── script-get_time.py ├── tests ├── test_Exchanges.py └── test_TradingAccount.py ├── troubleshoot.py └── views ├── TradingGraphs.py └── __init__.py /.github/workflows/container.yml: -------------------------------------------------------------------------------- 1 | name: Container 2 | 3 | on: 4 | push: 5 | # Publish `main` as Docker `latest` image. 6 | branches: 7 | - main 8 | - docker 9 | 10 | # Publish `v1.2.3` tags as releases. 11 | tags: 12 | - 1* 13 | - 2* 14 | 15 | # Run tests for any PRs. 16 | pull_request: 17 | 18 | env: 19 | IMAGE_NAME: pycryptobot 20 | 21 | jobs: 22 | # Build and push image to GitHub Packages. 23 | # See also https://docs.docker.com/docker-hub/builds/ 24 | push: 25 | runs-on: ubuntu-latest 26 | if: github.event_name == 'push' 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Build image 32 | run: docker build . --file Dockerfile --tag $IMAGE_NAME 33 | 34 | - name: Log into registry 35 | run: echo "${{ secrets.PACKAGE_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin 36 | 37 | - name: Push image 38 | run: | 39 | IMAGE_ID=ghcr.io/${{ github.repository }}/$IMAGE_NAME 40 | 41 | # Change all uppercase to lowercase 42 | IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') 43 | 44 | # Strip git ref prefix from version 45 | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 46 | 47 | # Use Docker `latest` tag convention 48 | [ "$VERSION" == "main" ] && VERSION=latest 49 | 50 | echo IMAGE_ID=$IMAGE_ID 51 | echo VERSION=$VERSION 52 | 53 | docker tag $IMAGE_NAME $IMAGE_ID:$VERSION 54 | docker push $IMAGE_ID:$VERSION 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | .vscode 135 | 136 | config.json 137 | config-coinbasepro.json 138 | config-binance.json 139 | 140 | *.csv 141 | graphs/ 142 | 143 | .DS_Store 144 | 145 | .vs/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi8/ubi-minimal 2 | 3 | RUN microdnf install -y python38 git && \ 4 | rm -rf /var/cache/microdnf && \ 5 | mkdir /app 6 | 7 | COPY . /app/ 8 | 9 | WORKDIR /app 10 | 11 | RUN python3 -m pip install -r requirements.txt 12 | 13 | # Pass parameters to the container run or mount your config.json into /app/ 14 | ENTRYPOINT [ "python3", "-u", "pycryptobot.py" ] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Docker](https://github.com/whittlem/pycryptobot/actions/workflows/docker-publish.yml/badge.svg)](https://github.com/whittlem/pycryptobot/actions/workflows/docker-publish.yml) 2 | 3 | # Python Crypto Bot v2.0.0 (pycryptobot) 4 | 5 | ## What's New? 6 | 7 | * The bot can now use both Binance and Coinbase Pro exchanges 8 | * Optimised the bot mechanics for buy and sell signals 9 | * Added "smart switching" between 15 minute and 1 hour graphs 10 | * Added additional technical indicators and candlesticks 11 | * Improved visual graphs for analysis 12 | * The bot is now also packaged in a container image 13 | 14 | ## Introduction 15 | 16 | Follow me on Medium for updates! 17 | https://whittle.medium.com 18 | 19 | ## Optional Add-on 20 | 21 | Coinbase Pro Portfolio Tracker 22 | https://github.com/whittlem/coinbaseprotracker 23 | 24 | An all-in-one view of all your Coinbase Pro portfolios. Highly recommended 25 | if running multiple bots and keeping track of their progress. 26 | 27 | ## Prerequisites 28 | 29 | * When running in containers: a working docker/podman installation 30 | 31 | * Python 3.9.x installed -- https://installpython3.com (must be Python 3.9 or greater) 32 | 33 | % python3 --version 34 | 35 | Python 3.9.1 36 | 37 | * Python 3 PIP installed -- https://pip.pypa.io/en/stable/installing 38 | 39 | % python3 -m pip --version 40 | 41 | pip 21.0.1 from /usr/local/lib/python3.9/site-packages/pip (python 3.9) 42 | 43 | ## Installation 44 | 45 | ### Manual 46 | 47 | % git clone https://github.com/whittlem/pycryptobot 48 | % cd pycryptobot 49 | % python3 -m pip install -r requirements.txt 50 | 51 | ### Container 52 | 53 | Install Docker Desktop 54 | https://docs.docker.com/desktop 55 | 56 | % docker pull ghcr.io/whittlem/pycryptobot/pycryptobot:latest 57 | latest: Pulling from whittlem/pycryptobot/pycryptobot 58 | 8f403cb21126: Pull complete 59 | 65c0f2178ac8: Pull complete 60 | 1091bd628216: Pull complete 61 | cb1eb04426a4: Pull complete 62 | ec065b94ad1c: Pull complete 63 | Digest: sha256:031fd6c7b7b2d08a743127e5850bc3d9c97a46e02ed0878f4445012eaf0619d3 64 | Status: Downloaded newer image for ghcr.io/whittlem/pycryptobot/pycryptobot:latest 65 | ghcr.io/whittlem/pycryptobot/pycryptobot:latest 66 | 67 | ## Additional Information 68 | 69 | The "requirements.txt" was created with `python3 -m pip freeze` 70 | 71 | ## Run it 72 | 73 | Manual: 74 | 75 | % python3 pycryptobot.py 76 | 77 | Container: 78 | 79 | Example Local Absolute Path: /home/example/config.json 80 | Example Market: BTC-GBP 81 | 82 | Daemon: 83 | % docker run --name BTC-GBP -v /home/example/config.json:/app/config.json -d ghcr.io/whittlem/pycryptobot/pycryptobot:latest 84 | 85 | Example: 86 | % docker run --name BTC-GBP -v /Users/whittlem/Documents/Repos/Docker/config.json:/app/config.json -d ghcr.io/whittlem/pycryptobot/pycryptobot:latest --live 0 87 | e491ae4fdba28aa9e74802895adf5e856006c3c63cf854c657482a6562a1e15 88 | 89 | Interactive: 90 | % docker run --name BTC-GBP -v /home/example/config.json:/app/config.json -it ghcr.io/whittlem/pycryptobot/pycryptobot:latest 91 | 92 | List Processes: 93 | % docker ps 94 | 95 | Example: 96 | % docker ps 97 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 98 | e491ae4fdba2 ghcr.io/whittlem/pycryptobot/pycryptobot:latest "python3 pycryptobot…" 46 seconds ago Up 44 seconds BTC-GBP 99 | 100 | Container Shell: 101 | % docker exec -it BTC-GBP /bin/bash 102 | [root@e491ae4fdba2 app]# 103 | 104 | Build your own image (if necessary): 105 | docker build -t pycryptobot_BTC-GBP . 106 | 107 | Running the docker image: 108 | docker run -d --rm --name pycryptobot_BTC-GBP_container pycryptobot_BTC-GBP 109 | 110 | Typically I would save all my settings in the config.json but running from the command line I would usually run it like this. 111 | 112 | % python3 pycryptobot.py --market BTC-GBP --granularity 3600 --live 1 --verbose 0 --selllowerpcnt -2 113 | 114 | ## Bot mechanics 115 | 116 | Smart switching: 117 | 118 | * If the EMA12 is greater than the EMA26 on the 1 hour and 6 hour intervals switch to start trading on the 15 minute intervals 119 | * If the EMA12 is lower than the EMA26 on the 1 hour and 6 hour intervals switch back to trade on the 1 hour intervals 120 | * If a "granularity" is specified as an argument or in the config.json then smart switching will be disabled 121 | * Force smart switching between 1 hour and 15 minute intervals with "smartswitch" argument or config option (1 or 0) 122 | 123 | Buy signal: 124 | 125 | * EMA12 is currently crossing above the EMA26 126 | * MACD is above the Signal 127 | * Golden Cross (SMA50 is above the SMA200) <-- bull market detection 128 | * Elder Ray Buy is True <-- bull market detection 129 | 130 | The bot will only trade in a bull market to minimise losses! 131 | 132 | Sell signal: 133 | 134 | * EMA12 is currently crossing below the EMA26 135 | * MACD is below the Signal 136 | 137 | Special sell cases: 138 | 139 | * If "sellatloss" is on, bot will sell if price drops below the lower Fibonacci band 140 | * If "sellatloss" is on and "selllowerpcnt" is specified the bot will sell at the specified amount E.g. -2 for -2% margin 141 | * If "sellupperpcnt" is specified the bot will sell at the specified amount E.g. 10 for 10% margin (Depending on the conditions I lock in profit at 3%) 142 | * If the margin exceeds 3% and the price reaches a Fibonacci band it will sell to lock in profit 143 | * If the margin exceeds 3% but a strong reversal is detected with negative OBV and MACD < Signal it will sell 144 | * "sellatloss" set to 0 prevents selling at a loss 145 | 146 | ## Disabling Default Functionality 147 | 148 | --disablebullonly Disable only buying in bull market 149 | --disablebuyobv Disable obv buy signal 150 | --disablebuyelderray Disable elder ray buy signal 151 | --disablecryptorecession Disable crypto recession check 152 | --disablefailsafefibonaccilow Disable failsafe sell on fibonacci lower band 153 | --disablefailsafelowerpcnt Disable failsafe sell on 'selllowerpcnt' 154 | --disableprofitbankupperpcnt Disable profit bank on 'sellupperpcnt' 155 | --disableprofitbankfibonaccihigh Disable profit bank on fibonacci upper band 156 | --disableprofitbankreversal Disable profit bank on strong candlestick reversal 157 | 158 | ## "Sell At Loss" explained 159 | 160 | The "sellatloss" option disabled has it's benefits and disadvantages. It does prevent any losses but also prevents you from exiting a market before a crash or bear market. Sometimes it's better to make an occasional small loss and make it up with several buys than be conservative and potentially lock a trade for weeks if not months. It happened to me while testing this with the last crash (after Elon's tweet!). Three of my bots did not sell while the profit dropped to -10 to -20%. It did bounce back and I made over 3% a trade with any losses but I also lost out of loads of trading opportunities. It's really a matter of preference. Maybe some markets would be more appropriate than others for this. 161 | 162 | ## Live Trading 163 | 164 | In order to trade live you need to authenticate with the Coinbase Pro or Binance APIs. This is all documented in my Medium articles. In summary you will need to include a config.json file in your project root which contains your API keys. If the file does not exist it will only work in test/demo mode. 165 | 166 | ## config.json examples 167 | 168 | Coinbase Pro basic (using smart switching) 169 | 170 | { 171 | "api_url" : "https://api.pro.coinbase.com", 172 | "api_key" : "", 173 | "api_secret" : "", 174 | "api_pass" : "", 175 | "config" : { 176 | "cryptoMarket" : "BTC", 177 | "fiatMarket" : "GBP", 178 | "live" : 1, 179 | "sellatloss" : 0 180 | } 181 | } 182 | 183 | Coinbase Pro basic (specific granularity, no smart switching) 184 | 185 | { 186 | "api_url" : "https://api.pro.coinbase.com", 187 | "api_key" : "", 188 | "api_secret" : "", 189 | "api_pass" : "", 190 | "config" : { 191 | "cryptoMarket" : "BCH", 192 | "fiatMarket" : "GBP", 193 | "granularity" : 3600, 194 | "live" : 1, 195 | "sellatloss" : 0 196 | } 197 | } 198 | 199 | Coinbase Pro only (new format) 200 | 201 | { 202 | "coinbasepro" : { 203 | "api_url" : "https://api.pro.coinbase.com", 204 | "api_key" : "", 205 | "api_secret" : "", 206 | "api_passphrase" : "", 207 | "config" : { 208 | "base_currency" : "BTC", 209 | "quote_currency" : "GBP", 210 | "granularity" : "3600", 211 | "live" : 0, 212 | "verbose" : 0 213 | } 214 | } 215 | } 216 | 217 | Binance only (new format) 218 | 219 | { 220 | "binance" : { 221 | "api_url" : "https://api.binance.com", 222 | "api_key" : "", 223 | "api_secret" : "", 224 | "config" : { 225 | "base_currency" : "BTC", 226 | "quote_currency" : "ZAR", 227 | "granularity" : "1h", 228 | "live" : 0, 229 | "verbose" : 0 230 | } 231 | } 232 | } 233 | 234 | Coinbase Pro and Binance (new format) 235 | 236 | { 237 | "binance" : { 238 | "api_url" : "https://api.binance.com", 239 | "api_key" : "", 240 | "api_secret" : "", 241 | "config" : { 242 | "base_currency" : "BTC", 243 | "quote_currency" : "ZAR", 244 | "granularity" : "1h", 245 | "live" : 0, 246 | "verbose" : 0 247 | } 248 | }, 249 | "coinbasepro" : { 250 | "api_url" : "https://api.pro.coinbase.com", 251 | "api_key" : "", 252 | "api_secret" : "", 253 | "api_passphrase" : "", 254 | "config" : { 255 | "base_currency" : "BTC", 256 | "quote_currency" : "GBP", 257 | "granularity" : "3600", 258 | "live" : 0, 259 | "verbose" : 0 260 | } 261 | } 262 | } 263 | 264 | All the "config" options in the config.json can be passed as arguments E.g. --market 265 | 266 | Command line arguments override config.json config. 267 | 268 | For telegram, add a piece to the config.json as follows: 269 | 270 | "telegram" : { 271 | "token" : "", 272 | "client_id" : "" 273 | } 274 | 275 | You can use @botfather and @myidbot in telegram to create a bot with token and get a client id. 276 | 277 | ## Multi-Market Trading 278 | 279 | The bot can trade mutiple markets at once. This is also documented in my Medium articles. The bot will execute buys using the full "quote currency" balance it has access too and it will sell the full "base currency" balance it has access too. In order to ring-fence your non-bot funds you should create another "Portfolio" in Coinbase Pro and assign API keys to it. That way you limit exposure. You can so something similar with Binance using sub-accounts but I believe you need to be a certain level to do this. 280 | 281 | The way you trade multiple markets at once is create multiple Coinbase Pro portfolios for each each bot instance. You will then clone this project for additional bots with the relevant Portfolio keys (config.json). 282 | 283 | I have 5 bots running at once for my Portfolios: "Bot - BTC-GBP", "Bot - BCH-GBP", "Bot - ETH-GBP", "Bot - ETH-GBP", and "Bot - XLM-EUR". 284 | 285 | Assuming each bot has a config.json that looks similar to this (update the "cryptoMarket" and "fiatMarket" appropriately): 286 | 287 | { 288 | "api_url" : "https://api.pro.coinbase.com", 289 | "api_key" : "", 290 | "api_secret" : "", 291 | "api_pass" : "", 292 | "config" : { 293 | "cryptoMarket" : "BTC", 294 | "fiatMarket" : "GBP", 295 | "live" : 1 296 | "selllowerpcnt" : -2 297 | } 298 | } 299 | 300 | The way I run my five bots is as follow: 301 | 302 | BTC-GBP % rm pycryptobot.log; git pull; clear; python3 pycryptobot.py 303 | 304 | BCH-GBP % rm pycryptobot.log; git pull; clear; python3 pycryptobot.py 305 | 306 | ETH-GBP % rm pycryptobot.log; git pull; clear; python3 pycryptobot.py 307 | 308 | LTC-GBP % rm pycryptobot.log; git pull; clear; python3 pycryptobot.py 309 | 310 | XLM-EUR % rm pycryptobot.log; git pull; clear; python3 pycryptobot.py 311 | 312 | Notice how I don't pass any arguments. It's all retrieved from the config.json but you can pass the arguments manually as well. 313 | 314 | ## The merge from "binance" branch back into "main" 315 | 316 | Some of you may have been helping test the new code for a few months in the "binance" branch. This is now merged back into the "main" branch. If you are still using the "binance" branch please carry out the following steps (per bot instance). 317 | 318 | git reset --hard 319 | git checkout main 320 | git pull 321 | python3 -m pip install -r requirements.txt 322 | 323 | Please note you need to be using Python 3.9.x or greater. The previous bot version only required Python 3.x. 324 | 325 | ## Upgrading the bots 326 | 327 | I push updates regularly and it's best to always be running the latest code. In each bot directory make sure you run this regularly. 328 | 329 | git pull 330 | 331 | I've actually included this in the examples in how to start the bot that will do this for you automatically. 332 | 333 | ## Fun quick non-live demo 334 | 335 | python3 pycryptobot.py --market BTC-GBP --granularity 3600 --sim fast --verbose 0 336 | 337 | If you get stuck with anything email me or raise an issue in the repo and I'll help you sort it out. Raising an issue is probably better as the question and response may help others. 338 | 339 | Enjoy and happy trading! :) 340 | -------------------------------------------------------------------------------- /create-graphs.py: -------------------------------------------------------------------------------- 1 | from models.PyCryptoBot import PyCryptoBot 2 | from models.Trading import TechnicalAnalysis 3 | from models.Binance import AuthAPI as BAuthAPI, PublicAPI as BPublicAPI 4 | from models.CoinbasePro import AuthAPI as CBAuthAPI, PublicAPI as CBPublicAPI 5 | from views.TradingGraphs import TradingGraphs 6 | 7 | #app = PyCryptoBot() 8 | app = PyCryptoBot('binance') 9 | tradingData = app.getHistoricalData(app.getMarket(), app.getGranularity()) 10 | 11 | technicalAnalysis = TechnicalAnalysis(tradingData) 12 | technicalAnalysis.addAll() 13 | 14 | tradinggraphs = TradingGraphs(technicalAnalysis) 15 | tradinggraphs.renderFibonacciRetracement(True) 16 | tradinggraphs.renderSupportResistance(True) 17 | tradinggraphs.renderCandlesticks(30, True) 18 | tradinggraphs.renderSeasonalARIMAModelPrediction(1, True) -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | pycryptobot: 5 | image: ghcr.io/whittlem/pycryptobot/pycryptobot:latest 6 | container_name: pycryptobot 7 | volumes: 8 | - ./config.json:/app/config.json 9 | - ./graphs:/app/graphs 10 | - /etc/localtime:/etc/localtime:ro 11 | # 12 | # You can also run multiple containers for each trading pair: 13 | # 14 | # pycryptobot_btceur: 15 | # image: ghcr.io/whittlem/pycryptobot:latest 16 | # container_name: pycryptobot 17 | # volumes: 18 | # - ./config_btceur.json:/app/config.json 19 | # - ./graphs:/app/graphs 20 | # - /etc/localtime:/etc/localtime:ro 21 | # 22 | # pycryptobot_etheur: 23 | # image: ghcr.io/whittlem/pycryptobot:latest 24 | # container_name: pycryptobot 25 | # volumes: 26 | # - ./config_etheur.json:/app/config.json 27 | # - ./graphs:/app/graphs 28 | # - /etc/localtime:/etc/localtime:ro 29 | -------------------------------------------------------------------------------- /graphs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rvn911/pycryptobot/b5bb0f54b56888788674f50740921cacf4025e3a/graphs/.gitkeep -------------------------------------------------------------------------------- /models/Binance.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import math, re 3 | import numpy as np 4 | import pandas as pd 5 | from datetime import datetime, timedelta 6 | from binance.client import Client 7 | 8 | class AuthAPIBase(): 9 | def _isMarketValid(self, market): 10 | p = re.compile(r"^[A-Z]{6,12}$") 11 | return p.match(market) 12 | 13 | class AuthAPI(AuthAPIBase): 14 | def __init__(self, api_key='', api_secret='', api_url='https://api.binance.com'): 15 | """Binance API object model 16 | 17 | Parameters 18 | ---------- 19 | api_key : str 20 | Your Binance account portfolio API key 21 | api_secret : str 22 | Your Binance account portfolio API secret 23 | """ 24 | 25 | # options 26 | self.debug = False 27 | self.die_on_api_error = False 28 | 29 | if len(api_url) > 1 and api_url[-1] != '/': 30 | api_url = api_url + '/' 31 | 32 | valid_urls = [ 33 | 'https://api.binance.com/', 34 | 'https://testnet.binance.vision/api/' 35 | ] 36 | 37 | # validate Binance API 38 | if api_url not in valid_urls: 39 | raise ValueError('Binance API URL is invalid') 40 | 41 | # validates the api key is syntactically correct 42 | p = re.compile(r"^[A-z0-9]{64,64}$") 43 | if not p.match(api_key): 44 | err = 'Binance API key is invalid' 45 | if self.debug: 46 | raise TypeError(err) 47 | else: 48 | raise SystemExit(err) 49 | 50 | # validates the api secret is syntactically correct 51 | p = re.compile(r"^[A-z0-9]{64,64}$") 52 | if not p.match(api_secret): 53 | err = 'Binance API secret is invalid' 54 | if self.debug: 55 | raise TypeError(err) 56 | else: 57 | raise SystemExit(err) 58 | 59 | self.mode = 'live' 60 | self.api_url = api_url 61 | self.api_key = api_key 62 | self.api_secret = api_secret 63 | self.client = Client(self.api_key, self.api_secret, { 'verify': False, 'timeout': 20 }) 64 | 65 | def getClient(self): 66 | return self.client 67 | 68 | def marketBuy(self, market='', quote_quantity=0): 69 | """Executes a market buy providing a funding amount""" 70 | 71 | # validates the market is syntactically correct 72 | if not self._isMarketValid(market): 73 | raise ValueError('Binance market is invalid.') 74 | 75 | # validates quote_quantity is either an integer or float 76 | if not isinstance(quote_quantity, int) and not isinstance(quote_quantity, float): 77 | raise TypeError('The funding amount is not numeric.') 78 | 79 | try: 80 | current_price = self.getTicker(market) 81 | 82 | base_quantity = np.divide(quote_quantity, current_price) 83 | df_filters = self.getMarketInfoFilters(market) 84 | step_size = float(df_filters.loc[df_filters['filterType'] == 'LOT_SIZE']['stepSize']) 85 | precision = int(round(-math.log(step_size, 10), 0)) 86 | 87 | # remove fees 88 | base_quantity = base_quantity * 0.9995 89 | 90 | # execute market buy 91 | stepper = 10.0 ** precision 92 | truncated = math.trunc(stepper * base_quantity) / stepper 93 | print ('Order quantity after rounding and fees:', truncated) 94 | return self.client.order_market_buy(symbol=market, quantity=truncated) 95 | except Exception as err: 96 | ts = datetime.now().strftime("%d-%m-%Y %H:%M:%S") 97 | print (ts, 'Binance', 'marketBuy', str(err)) 98 | return [] 99 | 100 | def marketSell(self, market='', base_quantity=0): 101 | """Executes a market sell providing a crypto amount""" 102 | 103 | # validates the market is syntactically correct 104 | if not self._isMarketValid(market): 105 | raise ValueError('Binance market is invalid.') 106 | 107 | if not isinstance(base_quantity, int) and not isinstance(base_quantity, float): 108 | raise TypeError('The crypto amount is not numeric.') 109 | 110 | try: 111 | df_filters = self.getMarketInfoFilters(market) 112 | step_size = float(df_filters.loc[df_filters['filterType'] == 'LOT_SIZE']['stepSize']) 113 | precision = int(round(-math.log(step_size, 10), 0)) 114 | 115 | # remove fees 116 | base_quantity = base_quantity * 0.9995 117 | 118 | # execute market sell 119 | stepper = 10.0 ** precision 120 | truncated = math.trunc(stepper * base_quantity) / stepper 121 | print ('Order quantity after rounding and fees:', truncated) 122 | return self.client.order_market_sell(symbol=market, quantity=truncated) 123 | except Exception as err: 124 | ts = datetime.now().strftime("%d-%m-%Y %H:%M:%S") 125 | print (ts, 'Binance', 'marketSell', str(err)) 126 | return [] 127 | 128 | def getMarketInfo(self, market): 129 | # validates the market is syntactically correct 130 | if not self._isMarketValid(market): 131 | raise TypeError('Binance market required.') 132 | 133 | return self.client.get_symbol_info(symbol=market) 134 | 135 | def getMarketInfoFilters(self, market): 136 | return pd.DataFrame(self.client.get_symbol_info(symbol=market)['filters']) 137 | 138 | def getTicker(self, market): 139 | # validates the market is syntactically correct 140 | if not self._isMarketValid(market): 141 | raise TypeError('Binance market required.') 142 | 143 | resp = self.client.get_symbol_ticker(symbol=market) 144 | 145 | if 'price' in resp: 146 | return float('{:.8f}'.format(float(resp['price']))) 147 | 148 | return 0.0 149 | 150 | def getTime(self): 151 | """Retrieves the exchange time""" 152 | 153 | try: 154 | resp = self.client.get_server_time() 155 | epoch = int(str(resp['serverTime'])[0:10]) 156 | return datetime.fromtimestamp(epoch) 157 | except: 158 | return None 159 | 160 | class PublicAPI(AuthAPIBase): 161 | def __init__(self): 162 | self.client = Client() 163 | 164 | def __truncate(self, f, n): 165 | return math.floor(f * 10 ** n) / 10 ** n 166 | 167 | def getClient(self): 168 | return self.client 169 | 170 | def getHistoricalData(self, market='BTCGBP', granularity='1h', iso8601start='', iso8601end=''): 171 | # validates the market is syntactically correct 172 | if not self._isMarketValid(market): 173 | raise TypeError('Binance market required.') 174 | 175 | # validates granularity is a string 176 | if not isinstance(granularity, str): 177 | raise TypeError('Granularity string required.') 178 | 179 | # validates the granularity is supported by Binance 180 | if not granularity in [ '1m', '5m', '15m', '1h', '6h', '1d' ]: 181 | raise TypeError('Granularity options: 1m, 5m, 15m. 1h, 6h, 1d') 182 | 183 | # validates the ISO 8601 start date is a string (if provided) 184 | if not isinstance(iso8601start, str): 185 | raise TypeError('ISO8601 start integer as string required.') 186 | 187 | # validates the ISO 8601 end date is a string (if provided) 188 | if not isinstance(iso8601end, str): 189 | raise TypeError('ISO8601 end integer as string required.') 190 | 191 | # if only a start date is provided 192 | if iso8601start != '' and iso8601end == '': 193 | multiplier = 1 194 | if(granularity == '1m'): 195 | multiplier = 1 196 | elif(granularity == '5m'): 197 | multiplier = 5 198 | elif(granularity == '15m'): 199 | multiplier = 15 200 | elif(granularity == '1h'): 201 | multiplier = 60 202 | elif(granularity == '6h'): 203 | multiplier = 360 204 | elif(granularity == '1d'): 205 | multiplier = 1440 206 | 207 | # calculate the end date using the granularity 208 | iso8601end = str((datetime.strptime(iso8601start, '%Y-%m-%dT%H:%M:%S.%f') + timedelta(minutes=granularity * multiplier)).isoformat()) 209 | 210 | if iso8601start != '' and iso8601end != '': 211 | print ('Attempting to retrieve data from ' + iso8601start) 212 | resp = self.client.get_historical_klines(market, granularity, iso8601start) 213 | 214 | if len(resp) > 300: 215 | resp = resp[:300] 216 | else: 217 | if granularity == '5m': 218 | resp = self.client.get_historical_klines(market, granularity, '2 days ago UTC') 219 | resp = resp[-300:] 220 | elif granularity == '15m': 221 | resp = self.client.get_historical_klines(market, granularity, '4 days ago UTC') 222 | resp = resp[-300:] 223 | elif granularity == '1h': 224 | resp = self.client.get_historical_klines(market, granularity, '13 days ago UTC') 225 | resp = resp[-300:] 226 | elif granularity == '6h': 227 | resp = self.client.get_historical_klines(market, granularity, '75 days ago UTC') 228 | resp = resp[-300:] 229 | elif granularity == '1d': 230 | resp = self.client.get_historical_klines(market, granularity, '251 days ago UTC') 231 | else: 232 | raise Exception('Something went wrong!') 233 | 234 | # convert the API response into a Pandas DataFrame 235 | df = pd.DataFrame(resp, columns=[ 'open_time', 'open', 'high', 'low', 'close', 'volume', 'close_time', 'quote_asset_volume', 'number_of_trades', 'taker_buy_base_asset_volume', 'traker_buy_quote_asset_volume', 'ignore' ]) 236 | df['market'] = market 237 | df['granularity'] = granularity 238 | 239 | # binance epoch is too long 240 | df['open_time'] = df['open_time'] + 1 241 | df['open_time'] = df['open_time'].astype(str) 242 | df['open_time'] = df['open_time'].str.replace(r'\d{3}$', '', regex=True) 243 | 244 | if(granularity == '1m'): 245 | freq = 'T' 246 | elif(granularity == '5m'): 247 | freq = '5T' 248 | elif(granularity == '15m'): 249 | freq = '15T' 250 | elif(granularity == '1h'): 251 | freq = 'H' 252 | elif(granularity == '6h'): 253 | freq = '6H' 254 | else: 255 | freq = 'D' 256 | 257 | # convert the DataFrame into a time series with the date as the index/key 258 | try: 259 | tsidx = pd.DatetimeIndex(pd.to_datetime(df['open_time'], unit='s'), dtype='datetime64[ns]', freq=freq) 260 | df.set_index(tsidx, inplace=True) 261 | df = df.drop(columns=['open_time']) 262 | df.index.names = ['ts'] 263 | df['date'] = tsidx 264 | except ValueError: 265 | tsidx = pd.DatetimeIndex(pd.to_datetime(df['open_time'], unit='s'), dtype='datetime64[ns]') 266 | df.set_index(tsidx, inplace=True) 267 | df = df.drop(columns=['open_time']) 268 | df.index.names = ['ts'] 269 | df['date'] = tsidx 270 | 271 | # re-order columns 272 | df = df[[ 'date', 'market', 'granularity', 'low', 'high', 'open', 'close', 'volume' ]] 273 | 274 | # correct column types 275 | df['low'] = df['low'].astype(float) 276 | df['high'] = df['high'].astype(float) 277 | df['open'] = df['open'].astype(float) 278 | df['close'] = df['close'].astype(float) 279 | df['volume'] = df['volume'].astype(float) 280 | 281 | # reset pandas dataframe index 282 | df.reset_index() 283 | 284 | return df 285 | 286 | def getTicker(self, market): 287 | # validates the market is syntactically correct 288 | if not self._isMarketValid(market): 289 | raise TypeError('Binance market required.') 290 | 291 | resp = self.client.get_symbol_ticker(symbol=market) 292 | 293 | if 'price' in resp: 294 | return float('{:.8f}'.format(float(resp['price']))) 295 | 296 | return 0.0 297 | 298 | def getTime(self): 299 | """Retrieves the exchange time""" 300 | 301 | try: 302 | resp = self.client.get_server_time() 303 | epoch = int(str(resp['serverTime'])[0:10]) 304 | return datetime.fromtimestamp(epoch) 305 | except: 306 | return None -------------------------------------------------------------------------------- /models/CoinbasePro.py: -------------------------------------------------------------------------------- 1 | """Remotely control your Coinbase Pro account via their API""" 2 | 3 | import pandas as pd 4 | import re, json, hmac, hashlib, time, requests, base64, sys 5 | from datetime import datetime, timedelta 6 | from requests.auth import AuthBase 7 | 8 | class AuthAPIBase(): 9 | def _isMarketValid(self, market): 10 | p = re.compile(r"^[1-9A-Z]{2,5}\-[1-9A-Z]{2,5}$") 11 | return p.match(market) 12 | 13 | class AuthAPI(AuthAPIBase): 14 | def __init__(self, api_key='', api_secret='', api_pass='', api_url='https://api.pro.coinbase.com'): 15 | """Coinbase Pro API object model 16 | 17 | Parameters 18 | ---------- 19 | api_key : str 20 | Your Coinbase Pro account portfolio API key 21 | api_secret : str 22 | Your Coinbase Pro account portfolio API secret 23 | api_pass : str 24 | Your Coinbase Pro account portfolio API passphrase 25 | api_url 26 | Coinbase Pro API URL 27 | """ 28 | 29 | # options 30 | self.debug = False 31 | self.die_on_api_error = False 32 | 33 | valid_urls = [ 34 | 'https://api.pro.coinbase.com', 35 | 'https://api.pro.coinbase.com/' 36 | ] 37 | 38 | # validate Coinbase Pro API 39 | if api_url not in valid_urls: 40 | raise ValueError('Coinbase Pro API URL is invalid') 41 | 42 | if api_url[-1] != '/': 43 | api_url = api_url + '/' 44 | 45 | # validates the api key is syntactically correct 46 | p = re.compile(r"^[a-f0-9]{32,32}$") 47 | if not p.match(api_key): 48 | err = 'Coinbase Pro API key is invalid' 49 | if self.debug: 50 | raise TypeError(err) 51 | else: 52 | raise SystemExit(err) 53 | 54 | # validates the api secret is syntactically correct 55 | p = re.compile(r"^[A-z0-9+\/]+==$") 56 | if not p.match(api_secret): 57 | err = 'Coinbase Pro API secret is invalid' 58 | if self.debug: 59 | raise TypeError(err) 60 | else: 61 | raise SystemExit(err) 62 | 63 | # validates the api passphase is syntactically correct 64 | p = re.compile(r"^[a-z0-9]{10,11}$") 65 | if not p.match(api_pass): 66 | err = 'Coinbase Pro API passphrase is invalid' 67 | if self.debug: 68 | raise TypeError(err) 69 | else: 70 | raise SystemExit(err) 71 | 72 | self.api_key = api_key 73 | self.api_secret = api_secret 74 | self.api_pass = api_pass 75 | self.api_url = api_url 76 | 77 | def __call__(self, request): 78 | """Signs the request""" 79 | 80 | timestamp = str(time.time()) 81 | message = timestamp + request.method + request.path_url + (request.body or b'').decode() 82 | hmac_key = base64.b64decode(self.api_secret) 83 | signature = hmac.new(hmac_key, message.encode(), hashlib.sha256) 84 | signature_b64 = base64.b64encode(signature.digest()).decode() 85 | 86 | request.headers.update({ 87 | 'CB-ACCESS-SIGN': signature_b64, 88 | 'CB-ACCESS-TIMESTAMP': timestamp, 89 | 'CB-ACCESS-KEY': self.api_key, 90 | 'CB-ACCESS-PASSPHRASE': self.api_pass, 91 | 'Content-Type': 'application/json' 92 | }) 93 | 94 | return request 95 | 96 | def getAccounts(self): 97 | """Retrieves your list of accounts""" 98 | 99 | # GET /accounts 100 | df = self.authAPI('GET', 'accounts') 101 | 102 | if len(df) == 0: 103 | return pd.DataFrame() 104 | 105 | # exclude accounts with a nil balance 106 | df = df[df.balance != '0.0000000000000000'] 107 | 108 | # reset the dataframe index to start from 0 109 | df = df.reset_index() 110 | return df 111 | 112 | def getAccount(self, account): 113 | """Retrieves a specific account""" 114 | 115 | # validates the account is syntactically correct 116 | p = re.compile(r"^[a-f0-9\-]{36,36}$") 117 | if not p.match(account): 118 | err = 'Coinbase Pro account is invalid' 119 | if self.debug: 120 | raise TypeError(err) 121 | else: 122 | raise SystemExit(err) 123 | 124 | return self.authAPI('GET', 'accounts/' + account) 125 | 126 | def getFees(self): 127 | return self.authAPI('GET', 'fees') 128 | 129 | def getMakerFee(self): 130 | fees = self.getFees() 131 | return float(fees['maker_fee_rate'].to_string(index=False).strip()) 132 | 133 | def getTakerFee(self): 134 | fees = self.getFees() 135 | return float(fees['taker_fee_rate'].to_string(index=False).strip()) 136 | 137 | def getUSDVolume(self): 138 | fees = self.getFees() 139 | return float(fees['usd_volume'].to_string(index=False).strip()) 140 | 141 | def getOrders(self, market='', action='', status='all'): 142 | """Retrieves your list of orders with optional filtering""" 143 | 144 | # if market provided 145 | if market != '': 146 | # validates the market is syntactically correct 147 | if not self._isMarketValid(market): 148 | raise ValueError('Coinbase Pro market is invalid.') 149 | 150 | # if action provided 151 | if action != '': 152 | # validates action is either a buy or sell 153 | if not action in ['buy', 'sell']: 154 | raise ValueError('Invalid order action.') 155 | 156 | # validates status is either open, pending, done, active, or all 157 | if not status in ['open', 'pending', 'done', 'active', 'all']: 158 | raise ValueError('Invalid order status.') 159 | 160 | # GET /orders?status 161 | resp = self.authAPI('GET', 'orders?status=' + status) 162 | if len(resp) > 0: 163 | if status == 'open': 164 | df = resp.copy()[[ 'created_at', 'product_id', 'side', 'type', 'size', 'price', 'status' ]] 165 | df['value'] = float(df['price']) * float(df['size']) 166 | else: 167 | df = resp.copy()[[ 'created_at', 'product_id', 'side', 'type', 'filled_size', 'executed_value', 'fill_fees', 'status' ]] 168 | else: 169 | return pd.DataFrame() 170 | 171 | # calculates the price at the time of purchase 172 | if status != 'open': 173 | df['price'] = df.apply(lambda row: (float(row.executed_value) * 100) / (float(row.filled_size) * 100) if float(row.filled_size) > 0 else 0, axis=1) 174 | 175 | # rename the columns 176 | if status == 'open': 177 | df.columns = [ 'created_at', 'market', 'action', 'type', 'size', 'price', 'status', 'value' ] 178 | df = df[[ 'created_at', 'market', 'action', 'type', 'size', 'value', 'status', 'price' ]] 179 | else: 180 | df.columns = [ 'created_at', 'market', 'action', 'type', 'size', 'value', 'fees', 'status', 'price' ] 181 | df = df[[ 'created_at', 'market', 'action', 'type', 'size', 'value', 'fees', 'price', 'status' ]] 182 | df['fees'] = df['fees'].astype(float).round(2) 183 | 184 | df['size'] = df['size'].astype(float) 185 | df['value'] = df['value'].astype(float) 186 | df['price'] = df['price'].astype(float) 187 | 188 | # convert dataframe to a time series 189 | tsidx = pd.DatetimeIndex(pd.to_datetime(df['created_at']).dt.strftime('%Y-%m-%dT%H:%M:%S.%Z')) 190 | df.set_index(tsidx, inplace=True) 191 | df = df.drop(columns=['created_at']) 192 | 193 | # if marker provided 194 | if market != '': 195 | # filter by market 196 | df = df[df['market'] == market] 197 | 198 | # if action provided 199 | if action != '': 200 | # filter by action 201 | df = df[df['action'] == action] 202 | 203 | # if status provided 204 | if status != 'all': 205 | # filter by status 206 | df = df[df['status'] == status] 207 | 208 | # reverse orders and reset index 209 | df = df.iloc[::-1].reset_index() 210 | 211 | # converts size and value to numeric type 212 | df[['size', 'value']] = df[['size', 'value']].apply(pd.to_numeric) 213 | return df 214 | 215 | def getTime(self): 216 | """Retrieves the exchange time""" 217 | 218 | try: 219 | resp = self.authAPI('GET', 'time') 220 | epoch = int(resp['epoch']) 221 | return datetime.fromtimestamp(epoch) 222 | except: 223 | return None 224 | 225 | def marketBuy(self, market='', quote_quantity=0): 226 | """Executes a market buy providing a funding amount""" 227 | 228 | # validates the market is syntactically correct 229 | if not self._isMarketValid(market): 230 | raise ValueError('Coinbase Pro market is invalid.') 231 | 232 | # validates quote_quantity is either an integer or float 233 | if not isinstance(quote_quantity, int) and not isinstance(quote_quantity, float): 234 | raise TypeError('The funding amount is not numeric.') 235 | 236 | # funding amount needs to be greater than 10 237 | if quote_quantity < 10: 238 | raise ValueError('Trade amount is too small (>= 10).') 239 | 240 | order = { 241 | 'product_id': market, 242 | 'type': 'market', 243 | 'side': 'buy', 244 | 'funds': quote_quantity 245 | } 246 | 247 | if self.debug == True: 248 | print (order) 249 | 250 | # connect to authenticated coinbase pro api 251 | model = AuthAPI(self.api_key, self.api_secret, self.api_pass, self.api_url) 252 | 253 | # place order and return result 254 | return model.authAPI('POST', 'orders', order) 255 | 256 | def marketSell(self, market='', base_quantity=0): 257 | if not self._isMarketValid(market): 258 | raise ValueError('Coinbase Pro market is invalid.') 259 | 260 | if not isinstance(base_quantity, int) and not isinstance(base_quantity, float): 261 | raise TypeError('The crypto amount is not numeric.') 262 | 263 | order = { 264 | 'product_id': market, 265 | 'type': 'market', 266 | 'side': 'sell', 267 | 'size': base_quantity 268 | } 269 | 270 | print (order) 271 | 272 | model = AuthAPI(self.api_key, self.api_secret, self.api_pass, self.api_url) 273 | return model.authAPI('POST', 'orders', order) 274 | 275 | def limitSell(self, market='', base_quantity=0, futurePrice=0): 276 | if not self._isMarketValid(market): 277 | raise ValueError('Coinbase Pro market is invalid.') 278 | 279 | if not isinstance(base_quantity, int) and not isinstance(base_quantity, float): 280 | raise TypeError('The crypto amount is not numeric.') 281 | 282 | if not isinstance(base_quantity, int) and not isinstance(base_quantity, float): 283 | raise TypeError('The future crypto price is not numeric.') 284 | 285 | order = { 286 | 'product_id': market, 287 | 'type': 'limit', 288 | 'side': 'sell', 289 | 'size': base_quantity, 290 | 'price': futurePrice 291 | } 292 | 293 | print (order) 294 | 295 | model = AuthAPI(self.api_key, self.api_secret, self.api_pass, self.api_url) 296 | return model.authAPI('POST', 'orders', order) 297 | 298 | def cancelOrders(self, market=''): 299 | if not self._isMarketValid(market): 300 | raise ValueError('Coinbase Pro market is invalid.') 301 | 302 | model = AuthAPI(self.api_key, self.api_secret, self.api_pass, self.api_url) 303 | return model.authAPI('DELETE', 'orders') 304 | 305 | def authAPI(self, method, uri, payload=''): 306 | if not isinstance(method, str): 307 | raise TypeError('Method is not a string.') 308 | 309 | if not method in ['DELETE','GET','POST']: 310 | raise TypeError('Method not DELETE, GET or POST.') 311 | 312 | if not isinstance(uri, str): 313 | raise TypeError('Method is not a string.') 314 | 315 | try: 316 | if method == 'DELETE': 317 | resp = requests.delete(self.api_url + uri, auth=self) 318 | elif method == 'GET': 319 | resp = requests.get(self.api_url + uri, auth=self) 320 | elif method == 'POST': 321 | resp = requests.post(self.api_url + uri, json=payload, auth=self) 322 | 323 | if resp.status_code != 200: 324 | if self.die_on_api_error: 325 | raise Exception(method.upper() + 'GET (' + '{}'.format(resp.status_code) + ') ' + self.api_url + uri + ' - ' + '{}'.format(resp.json()['message'])) 326 | else: 327 | print ('error:', method.upper() + ' (' + '{}'.format(resp.status_code) + ') ' + self.api_url + uri + ' - ' + '{}'.format(resp.json()['message'])) 328 | return pd.DataFrame() 329 | 330 | resp.raise_for_status() 331 | json = resp.json() 332 | 333 | if isinstance(json, list): 334 | df = pd.DataFrame.from_dict(json) 335 | return df 336 | else: 337 | df = pd.DataFrame(json, index=[0]) 338 | return df 339 | 340 | except requests.ConnectionError as err: 341 | if self.debug: 342 | if self.die_on_api_error: 343 | raise SystemExit(err) 344 | else: 345 | print (err) 346 | return pd.DataFrame() 347 | else: 348 | if self.die_on_api_error: 349 | raise SystemExit('ConnectionError: ' + self.api_url) 350 | else: 351 | print ('ConnectionError: ' + self.api_url) 352 | return pd.DataFrame() 353 | 354 | except requests.exceptions.HTTPError as err: 355 | if self.debug: 356 | if self.die_on_api_error: 357 | raise SystemExit(err) 358 | else: 359 | print (err) 360 | return pd.DataFrame() 361 | else: 362 | if self.die_on_api_error: 363 | raise SystemExit('HTTPError: ' + self.api_url) 364 | else: 365 | print ('HTTPError: ' + self.api_url) 366 | return pd.DataFrame() 367 | 368 | except requests.Timeout as err: 369 | if self.debug: 370 | if self.die_on_api_error: 371 | raise SystemExit(err) 372 | else: 373 | print (err) 374 | return pd.DataFrame() 375 | else: 376 | if self.die_on_api_error: 377 | raise SystemExit('Timeout: ' + self.api_url) 378 | else: 379 | print ('Timeout: ' + self.api_url) 380 | return pd.DataFrame() 381 | 382 | class PublicAPI(AuthAPIBase): 383 | def __init__(self): 384 | # options 385 | self.debug = False 386 | self.die_on_api_error = False 387 | 388 | self.api_url = 'https://api.pro.coinbase.com/' 389 | 390 | def getHistoricalData(self, market='BTC-GBP', granularity=86400, iso8601start='', iso8601end=''): 391 | # validates the market is syntactically correct 392 | if not self._isMarketValid(market): 393 | raise TypeError('Coinbase Pro market required.') 394 | 395 | # validates granularity is an integer 396 | if not isinstance(granularity, int): 397 | raise TypeError('Granularity integer required.') 398 | 399 | # validates the granularity is supported by Coinbase Pro 400 | if not granularity in [ 60, 300, 900, 3600, 21600, 86400 ]: 401 | raise TypeError('Granularity options: 60, 300, 900, 3600, 21600, 86400') 402 | 403 | # validates the ISO 8601 start date is a string (if provided) 404 | if not isinstance(iso8601start, str): 405 | raise TypeError('ISO8601 start integer as string required.') 406 | 407 | # validates the ISO 8601 end date is a string (if provided) 408 | if not isinstance(iso8601end, str): 409 | raise TypeError('ISO8601 end integer as string required.') 410 | 411 | # if only a start date is provided 412 | if iso8601start != '' and iso8601end == '': 413 | multiplier = 1 414 | if(granularity == 60): 415 | multiplier = 1 416 | elif(granularity == 300): 417 | multiplier = 5 418 | elif(granularity == 900): 419 | multiplier = 10 420 | elif(granularity == 3600): 421 | multiplier = 60 422 | elif(granularity == 21600): 423 | multiplier = 360 424 | elif(granularity == 86400): 425 | multiplier = 1440 426 | 427 | # calculate the end date using the granularity 428 | iso8601end = str((datetime.strptime(iso8601start, '%Y-%m-%dT%H:%M:%S.%f') + timedelta(minutes=granularity * multiplier)).isoformat()) 429 | 430 | resp = self.authAPI('GET','products/' + market + '/candles?granularity=' + str(granularity) + '&start=' + iso8601start + '&end=' + iso8601end) 431 | 432 | # convert the API response into a Pandas DataFrame 433 | df = pd.DataFrame(resp, columns=[ 'epoch', 'low', 'high', 'open', 'close', 'volume' ]) 434 | # reverse the order of the response with earliest last 435 | df = df.iloc[::-1].reset_index() 436 | 437 | if(granularity == 60): 438 | freq = 'T' 439 | elif(granularity == 300): 440 | freq = '5T' 441 | elif(granularity == 900): 442 | freq = '15T' 443 | elif(granularity == 3600): 444 | freq = 'H' 445 | elif(granularity == 21600): 446 | freq = '6H' 447 | else: 448 | freq = 'D' 449 | 450 | # convert the DataFrame into a time series with the date as the index/key 451 | try: 452 | tsidx = pd.DatetimeIndex(pd.to_datetime(df['epoch'], unit='s'), dtype='datetime64[ns]', freq=freq) 453 | df.set_index(tsidx, inplace=True) 454 | df = df.drop(columns=['epoch','index']) 455 | df.index.names = ['ts'] 456 | df['date'] = tsidx 457 | except ValueError: 458 | tsidx = pd.DatetimeIndex(pd.to_datetime(df['epoch'], unit='s'), dtype='datetime64[ns]') 459 | df.set_index(tsidx, inplace=True) 460 | df = df.drop(columns=['epoch','index']) 461 | df.index.names = ['ts'] 462 | df['date'] = tsidx 463 | 464 | df['market'] = market 465 | df['granularity'] = granularity 466 | 467 | # re-order columns 468 | df = df[[ 'date', 'market', 'granularity', 'low', 'high', 'open', 'close', 'volume' ]] 469 | 470 | return df 471 | 472 | def getTicker(self, market='BTC-GBP'): 473 | # validates the market is syntactically correct 474 | if not self._isMarketValid(market): 475 | raise TypeError('Coinbase Pro market required.') 476 | 477 | resp = self.authAPI('GET','products/' + market + '/ticker') 478 | if 'price' in resp: 479 | return float(resp['price']) 480 | 481 | return 0.0 482 | 483 | def getTime(self): 484 | """Retrieves the exchange time""" 485 | 486 | try: 487 | resp = self.authAPI('GET', 'time') 488 | epoch = int(resp['epoch']) 489 | return datetime.fromtimestamp(epoch) 490 | except: 491 | return None 492 | 493 | def authAPI(self, method, uri, payload=''): 494 | if not isinstance(method, str): 495 | raise TypeError('Method is not a string.') 496 | 497 | if not method in ['GET', 'POST']: 498 | raise TypeError('Method not GET or POST.') 499 | 500 | if not isinstance(uri, str): 501 | raise TypeError('Method is not a string.') 502 | 503 | try: 504 | if method == 'GET': 505 | resp = requests.get(self.api_url + uri) 506 | elif method == 'POST': 507 | resp = requests.post(self.api_url + uri, json=payload) 508 | 509 | if resp.status_code != 200: 510 | if self.die_on_api_error: 511 | raise Exception(method.upper() + 'GET (' + '{}'.format(resp.status_code) + ') ' + self.api_url + uri + ' - ' + '{}'.format(resp.json()['message'])) 512 | else: 513 | print('error:', method.upper() + ' (' + '{}'.format(resp.status_code) + ') ' + self.api_url + uri + ' - ' + '{}'.format(resp.json()['message'])) 514 | return pd.DataFrame() 515 | 516 | resp.raise_for_status() 517 | json = resp.json() 518 | return json 519 | 520 | except requests.ConnectionError as err: 521 | if self.debug: 522 | if self.die_on_api_error: 523 | raise SystemExit(err) 524 | else: 525 | print(err) 526 | return pd.DataFrame() 527 | else: 528 | if self.die_on_api_error: 529 | raise SystemExit('ConnectionError: ' + self.api_url) 530 | else: 531 | print('ConnectionError: ' + self.api_url) 532 | return pd.DataFrame() 533 | 534 | except requests.exceptions.HTTPError as err: 535 | if self.debug: 536 | if self.die_on_api_error: 537 | raise SystemExit(err) 538 | else: 539 | print(err) 540 | return pd.DataFrame() 541 | else: 542 | if self.die_on_api_error: 543 | raise SystemExit('HTTPError: ' + self.api_url) 544 | else: 545 | print('HTTPError: ' + self.api_url) 546 | return pd.DataFrame() 547 | 548 | except requests.Timeout as err: 549 | if self.debug: 550 | if self.die_on_api_error: 551 | raise SystemExit(err) 552 | else: 553 | print(err) 554 | return pd.DataFrame() 555 | else: 556 | if self.die_on_api_error: 557 | raise SystemExit('Timeout: ' + self.api_url) 558 | else: 559 | print('Timeout: ' + self.api_url) 560 | return pd.DataFrame() 561 | -------------------------------------------------------------------------------- /models/Telegram.py: -------------------------------------------------------------------------------- 1 | import requests, re 2 | 3 | class Telegram(): 4 | def __init__(self, token='', client_id=''): 5 | self.api = 'https://api.telegram.org/bot' 6 | self._token = token 7 | self._client_id = str(client_id) 8 | 9 | p = re.compile(r"^\d{1,10}:[A-z0-9-_]{35,35}$") 10 | if not p.match(token): 11 | raise Exception('Telegram token is invalid') 12 | 13 | p = re.compile(r"^-*\d{7,10}$") 14 | if not p.match(client_id): 15 | raise Exception('Telegram client_id is invalid') 16 | 17 | def send(self, message=''): 18 | try: 19 | payload = self.api + self._token + '/sendMessage?chat_id=' + self._client_id + '&parse_mode=Markdown&text=' + message 20 | resp = requests.get(payload) 21 | 22 | if resp.status_code != 200: 23 | return None 24 | 25 | resp.raise_for_status() 26 | json = resp.json() 27 | 28 | except requests.ConnectionError as err: 29 | print (err) 30 | return ('') 31 | 32 | except requests.exceptions.HTTPError as err: 33 | print (err) 34 | return ('') 35 | 36 | except requests.Timeout as err: 37 | print (err) 38 | return ('') 39 | 40 | return json -------------------------------------------------------------------------------- /models/Trading.py: -------------------------------------------------------------------------------- 1 | """Technical analysis on a trading Pandas DataFrame""" 2 | 3 | import json, math 4 | import numpy as np 5 | import pandas as pd 6 | import re, sys 7 | from statsmodels.tsa.statespace.sarimax import SARIMAX 8 | from models.CoinbasePro import AuthAPI 9 | 10 | class TechnicalAnalysis(): 11 | def __init__(self, data=pd.DataFrame()): 12 | """Technical Analysis object model 13 | 14 | Parameters 15 | ---------- 16 | data : Pandas Time Series 17 | data[ts] = [ 'date', 'market', 'granularity', 'low', 'high', 'open', 'close', 'volume' ] 18 | """ 19 | 20 | if not isinstance(data, pd.DataFrame): 21 | raise TypeError('Data is not a Pandas dataframe.') 22 | 23 | if list(data.keys()) != [ 'date', 'market', 'granularity', 'low', 'high', 'open', 'close', 'volume' ]: 24 | raise ValueError('Data not not contain date, market, granularity, low, high, open, close, volume') 25 | 26 | if not 'close' in data.columns: 27 | raise AttributeError("Pandas DataFrame 'close' column required.") 28 | 29 | if not data['close'].dtype == 'float64' and not data['close'].dtype == 'int64': 30 | raise AttributeError("Pandas DataFrame 'close' column not int64 or float64.") 31 | 32 | self.df = data 33 | self.levels = [] 34 | 35 | def getDataFrame(self): 36 | """Returns the Pandas DataFrame""" 37 | 38 | return self.df 39 | 40 | def addAll(self): 41 | """Adds analysis to the DataFrame""" 42 | 43 | self.addChangePct() 44 | 45 | self.addCMA() 46 | self.addSMA(20) 47 | self.addSMA(50) 48 | self.addSMA(200) 49 | self.addEMA(12) 50 | self.addEMA(26) 51 | self.addGoldenCross() 52 | self.addDeathCross() 53 | self.addFibonacciBollingerBands() 54 | 55 | self.addRSI(14) 56 | self.addMACD() 57 | self.addOBV() 58 | self.addElderRayIndex() 59 | 60 | self.addEMABuySignals() 61 | self.addSMABuySignals() 62 | self.addMACDBuySignals() 63 | 64 | self.addCandleAstralBuy() 65 | self.addCandleAstralSell() 66 | self.addCandleHammer() 67 | self.addCandleInvertedHammer() 68 | self.addCandleShootingStar() 69 | self.addCandleHangingMan() 70 | self.addCandleThreeWhiteSoldiers() 71 | self.addCandleThreeBlackCrows() 72 | self.addCandleDoji() 73 | self.addCandleThreeLineStrike() 74 | self.addCandleTwoBlackGapping() 75 | self.addCandleMorningStar() 76 | self.addCandleEveningStar() 77 | self.addCandleAbandonedBaby() 78 | self.addCandleMorningDojiStar() 79 | self.addCandleEveningDojiStar() 80 | 81 | """Candlestick References 82 | https://commodity.com/technical-analysis 83 | https://www.investopedia.com 84 | https://github.com/SpiralDevelopment/candlestick-patterns 85 | https://www.incrediblecharts.com/candlestick_patterns/candlestick-patterns-strongest.php 86 | """ 87 | 88 | def candleHammer(self): 89 | """* Candlestick Detected: Hammer ("Weak - Reversal - Bullish Signal - Up""" 90 | 91 | return ((self.df['high'] - self.df['low']) > 3 * (self.df['open'] - self.df['close'])) \ 92 | & (((self.df['close'] - self.df['low']) / (.001 + self.df['high'] - self.df['low'])) > 0.6) \ 93 | & (((self.df['open'] - self.df['low']) / (.001 + self.df['high'] - self.df['low'])) > 0.6) 94 | 95 | def addCandleHammer(self): 96 | self.df['hammer'] = self.candleHammer() 97 | 98 | def candleShootingStar(self): 99 | """* Candlestick Detected: Shooting Star ("Weak - Reversal - Bearish Pattern - Down")""" 100 | 101 | return ((self.df['open'].shift(1) < self.df['close'].shift(1)) & (self.df['close'].shift(1) < self.df['open'])) \ 102 | & (self.df['high'] - np.maximum(self.df['open'], self.df['close']) >= (abs(self.df['open'] - self.df['close']) * 3)) \ 103 | & ((np.minimum(self.df['close'], self.df['open']) - self.df['low']) <= abs(self.df['open'] - self.df['close'])) 104 | 105 | def addCandleShootingStar(self): 106 | self.df['shooting_star'] = self.candleShootingStar() 107 | 108 | def candleHangingMan(self): 109 | """* Candlestick Detected: Hanging Man ("Weak - Continuation - Bearish Pattern - Down")""" 110 | 111 | return ((self.df['high'] - self.df['low']) > (4 * (self.df['open'] - self.df['close']))) \ 112 | & (((self.df['close'] - self.df['low']) / (.001 + self.df['high'] - self.df['low'])) >= 0.75) \ 113 | & (((self.df['open'] - self.df['low']) / (.001 + self.df['high'] - self.df['low'])) >= 0.75) \ 114 | & (self.df['high'].shift(1) < self.df['open']) \ 115 | & (self.df['high'].shift(2) < self.df['open']) 116 | 117 | def addCandleHangingMan(self): 118 | self.df['hanging_man'] = self.candleHangingMan() 119 | 120 | def candleInvertedHammer(self): 121 | """* Candlestick Detected: Inverted Hammer ("Weak - Continuation - Bullish Pattern - Up")""" 122 | 123 | return (((self.df['high'] - self.df['low']) > 3 * (self.df['open'] - self.df['close'])) \ 124 | & ((self.df['high'] - self.df['close']) / (.001 + self.df['high'] - self.df['low']) > 0.6) \ 125 | & ((self.df['high'] - self.df['open']) / (.001 + self.df['high'] - self.df['low']) > 0.6)) 126 | 127 | def addCandleInvertedHammer(self): 128 | self.df['inverted_hammer'] = self.candleInvertedHammer() 129 | 130 | def candleThreeWhiteSoldiers(self): 131 | """*** Candlestick Detected: Three White Soldiers ("Strong - Reversal - Bullish Pattern - Up")""" 132 | 133 | return ((self.df['open'] > self.df['open'].shift(1)) & (self.df['open'] < self.df['close'].shift(1))) \ 134 | & (self.df['close'] > self.df['high'].shift(1)) \ 135 | & (self.df['high'] - np.maximum(self.df['open'], self.df['close']) < (abs(self.df['open'] - self.df['close']))) \ 136 | & ((self.df['open'].shift(1) > self.df['open'].shift(2)) & (self.df['open'].shift(1) < self.df['close'].shift(2))) \ 137 | & (self.df['close'].shift(1) > self.df['high'].shift(2)) \ 138 | & (self.df['high'].shift(1) - np.maximum(self.df['open'].shift(1), self.df['close'].shift(1)) < (abs(self.df['open'].shift(1) - self.df['close'].shift(1)))) 139 | 140 | def addCandleThreeWhiteSoldiers(self): 141 | self.df['three_white_soldiers'] = self.candleThreeWhiteSoldiers() 142 | 143 | def candleThreeBlackCrows(self): 144 | """* Candlestick Detected: Three Black Crows ("Strong - Reversal - Bearish Pattern - Down")""" 145 | 146 | return ((self.df['open'] < self.df['open'].shift(1)) & (self.df['open'] > self.df['close'].shift(1))) \ 147 | & (self.df['close'] < self.df['low'].shift(1)) \ 148 | & (self.df['low'] - np.maximum(self.df['open'], self.df['close']) < (abs(self.df['open'] - self.df['close']))) \ 149 | & ((self.df['open'].shift(1) < self.df['open'].shift(2)) & (self.df['open'].shift(1) > self.df['close'].shift(2))) \ 150 | & (self.df['close'].shift(1) < self.df['low'].shift(2)) \ 151 | & (self.df['low'].shift(1) - np.maximum(self.df['open'].shift(1), self.df['close'].shift(1)) < (abs(self.df['open'].shift(1) - self.df['close'].shift(1)))) 152 | 153 | def addCandleThreeBlackCrows(self): 154 | self.df['three_black_crows'] = self.candleThreeBlackCrows() 155 | 156 | def candleDoji(self): 157 | """! Candlestick Detected: Doji ("Indecision")""" 158 | 159 | return ((abs(self.df['close'] - self.df['open']) / (self.df['high'] - self.df['low'])) < 0.1) \ 160 | & ((self.df['high'] - np.maximum(self.df['close'], self.df['open'])) > (3 * abs(self.df['close'] - self.df['open']))) \ 161 | & ((np.minimum(self.df['close'], self.df['open']) - self.df['low']) > (3 * abs(self.df['close'] - self.df['open']))) 162 | 163 | def addCandleDoji(self): 164 | self.df['doji'] = self.candleDoji() 165 | 166 | def candleThreeLineStrike(self): 167 | """** Candlestick Detected: Three Line Strike ("Reliable - Reversal - Bullish Pattern - Up")""" 168 | 169 | return ((self.df['open'].shift(1) < self.df['open'].shift(2)) & (self.df['open'].shift(1) > self.df['close'].shift(2))) \ 170 | & (self.df['close'].shift(1) < self.df['low'].shift(2)) \ 171 | & (self.df['low'].shift(1) - np.maximum(self.df['open'].shift(1), self.df['close'].shift(1)) < (abs(self.df['open'].shift(1) - self.df['close'].shift(1)))) \ 172 | & ((self.df['open'].shift(2) < self.df['open'].shift(3)) & (self.df['open'].shift(2) > self.df['close'].shift(3))) \ 173 | & (self.df['close'].shift(2) < self.df['low'].shift(3)) \ 174 | & (self.df['low'].shift(2) - np.maximum(self.df['open'].shift(2), self.df['close'].shift(2)) < (abs(self.df['open'].shift(2) - self.df['close'].shift(2)))) \ 175 | & ((self.df['open'] < self.df['low'].shift(1)) & (self.df['close'] > self.df['high'].shift(3))) 176 | 177 | def addCandleThreeLineStrike(self): 178 | self.df['three_line_strike'] = self.candleThreeLineStrike() 179 | 180 | def candleTwoBlackGapping(self): 181 | """*** Candlestick Detected: Two Black Gapping ("Reliable - Reversal - Bearish Pattern - Down")""" 182 | 183 | return ((self.df['open'] < self.df['open'].shift(1)) & (self.df['open'] > self.df['close'].shift(1))) \ 184 | & (self.df['close'] < self.df['low'].shift(1)) \ 185 | & (self.df['low'] - np.maximum(self.df['open'], self.df['close']) < (abs(self.df['open'] - self.df['close']))) \ 186 | & (self.df['high'].shift(1) < self.df['low'].shift(2)) 187 | 188 | def addCandleTwoBlackGapping(self): 189 | self.df['two_black_gapping'] = self.candleTwoBlackGapping() 190 | 191 | def candleMorningStar(self): 192 | """*** Candlestick Detected: Morning Star ("Strong - Reversal - Bullish Pattern - Up")""" 193 | 194 | return ((np.maximum(self.df['open'].shift(1), self.df['close'].shift(1)) < self.df['close'].shift(2)) & (self.df['close'].shift(2) < self.df['open'].shift(2))) \ 195 | & ((self.df['close'] > self.df['open']) & (self.df['open'] > np.maximum(self.df['open'].shift(1), self.df['close'].shift(1)))) 196 | 197 | def addCandleMorningStar(self): 198 | self.df['morning_star'] = self.candleMorningStar() 199 | 200 | def candleEveningStar(self): 201 | """*** Candlestick Detected: Evening Star ("Strong - Reversal - Bearish Pattern - Down")""" 202 | 203 | return ((np.minimum(self.df['open'].shift(1), self.df['close'].shift(1)) > self.df['close'].shift(2)) & (self.df['close'].shift(2) > self.df['open'].shift(2))) \ 204 | & ((self.df['close'] < self.df['open']) & (self.df['open'] < np.minimum(self.df['open'].shift(1), self.df['close'].shift(1)))) 205 | 206 | def addCandleEveningStar(self): 207 | self.df['evening_star'] = self.candleEveningStar() 208 | 209 | def candleAbandonedBaby(self): 210 | """** Candlestick Detected: Abandoned Baby ("Reliable - Reversal - Bullish Pattern - Up")""" 211 | 212 | return (self.df['open'] < self.df['close']) \ 213 | & (self.df['high'].shift(1) < self.df['low']) \ 214 | & (self.df['open'].shift(2) > self.df['close'].shift(2)) \ 215 | & (self.df['high'].shift(1) < self.df['low'].shift(2)) 216 | 217 | def addCandleAbandonedBaby(self): 218 | self.df['abandoned_baby'] = self.candleAbandonedBaby() 219 | 220 | def candleMorningDojiStar(self): 221 | """** Candlestick Detected: Morning Doji Star ("Reliable - Reversal - Bullish Pattern - Up")""" 222 | 223 | return (self.df['close'].shift(2) < self.df['open'].shift(2)) \ 224 | & (abs(self.df['close'].shift(2) - self.df['open'].shift(2)) / (self.df['high'].shift(2) - self.df['low'].shift(2)) >= 0.7) \ 225 | & (abs(self.df['close'].shift(1) - self.df['open'].shift(1)) / (self.df['high'].shift(1) - self.df['low'].shift(1)) < 0.1) \ 226 | & (self.df['close'] > self.df['open']) \ 227 | & (abs(self.df['close'] - self.df['open']) / (self.df['high'] - self.df['low']) >= 0.7) \ 228 | & (self.df['close'].shift(2) > self.df['close'].shift(1)) \ 229 | & (self.df['close'].shift(2) > self.df['open'].shift(1)) \ 230 | & (self.df['close'].shift(1) < self.df['open']) \ 231 | & (self.df['open'].shift(1) < self.df['open']) \ 232 | & (self.df['close'] > self.df['close'].shift(2)) \ 233 | & ((self.df['high'].shift(1) - np.maximum(self.df['close'].shift(1), self.df['open'].shift(1))) > (3 * abs(self.df['close'].shift(1) - self.df['open'].shift(1)))) \ 234 | & (np.minimum(self.df['close'].shift(1), self.df['open'].shift(1)) - self.df['low'].shift(1)) > (3 * abs(self.df['close'].shift(1) - self.df['open'].shift(1))) 235 | 236 | def addCandleMorningDojiStar(self): 237 | self.df['morning_doji_star'] = self.candleMorningDojiStar() 238 | 239 | def candleEveningDojiStar(self): 240 | """** Candlestick Detected: Evening Doji Star ("Reliable - Reversal - Bearish Pattern - Down")""" 241 | 242 | return (self.df['close'].shift(2) > self.df['open'].shift(2)) \ 243 | & (abs(self.df['close'].shift(2) - self.df['open'].shift(2)) / (self.df['high'].shift(2) - self.df['low'].shift(2)) >= 0.7) \ 244 | & (abs(self.df['close'].shift(1) - self.df['open'].shift(1)) / (self.df['high'].shift(1) - self.df['low'].shift(1)) < 0.1) \ 245 | & (self.df['close'] < self.df['open']) \ 246 | & (abs(self.df['close'] - self.df['open']) / (self.df['high'] - self.df['low']) >= 0.7) \ 247 | & (self.df['close'].shift(2) < self.df['close'].shift(1)) \ 248 | & (self.df['close'].shift(2) < self.df['open'].shift(1)) \ 249 | & (self.df['close'].shift(1) > self.df['open']) \ 250 | & (self.df['open'].shift(1) > self.df['open']) \ 251 | & (self.df['close'] < self.df['close'].shift(2)) \ 252 | & ((self.df['high'].shift(1) - np.maximum(self.df['close'].shift(1), self.df['open'].shift(1))) > (3 * abs(self.df['close'].shift(1) - self.df['open'].shift(1)))) \ 253 | & (np.minimum(self.df['close'].shift(1), self.df['open'].shift(1)) - self.df['low'].shift(1)) > (3 * abs(self.df['close'].shift(1) - self.df['open'].shift(1))) 254 | 255 | def addCandleEveningDojiStar(self): 256 | self.df['evening_doji_star'] = self.candleEveningDojiStar() 257 | 258 | def candleAstralBuy(self): 259 | """*** Candlestick Detected: Astral Buy (Fibonacci 3, 5, 8)""" 260 | 261 | return (self.df['close'] < self.df['close'].shift(3)) & (self.df['low'] < self.df['low'].shift(5)) \ 262 | & (self.df['close'].shift(1) < self.df['close'].shift(4)) & (self.df['low'].shift(1) < self.df['low'].shift(6)) \ 263 | & (self.df['close'].shift(2) < self.df['close'].shift(5)) & (self.df['low'].shift(2) < self.df['low'].shift(7)) \ 264 | & (self.df['close'].shift(3) < self.df['close'].shift(6)) & (self.df['low'].shift(3) < self.df['low'].shift(8)) \ 265 | & (self.df['close'].shift(4) < self.df['close'].shift(7)) & (self.df['low'].shift(4) < self.df['low'].shift(9)) \ 266 | & (self.df['close'].shift(5) < self.df['close'].shift(8)) & (self.df['low'].shift(5) < self.df['low'].shift(10)) \ 267 | & (self.df['close'].shift(6) < self.df['close'].shift(9)) & (self.df['low'].shift(6) < self.df['low'].shift(11)) \ 268 | & (self.df['close'].shift(7) < self.df['close'].shift(10)) & (self.df['low'].shift(7) < self.df['low'].shift(12)) 269 | 270 | def addCandleAstralBuy(self): 271 | self.df['astral_buy'] = self.candleAstralBuy() 272 | 273 | def candleAstralSell(self): 274 | """*** Candlestick Detected: Astral Sell (Fibonacci 3, 5, 8)""" 275 | 276 | return (self.df['close'] > self.df['close'].shift(3)) & (self.df['high'] > self.df['high'].shift(5)) \ 277 | & (self.df['close'].shift(1) > self.df['close'].shift(4)) & (self.df['high'].shift(1) > self.df['high'].shift(6)) \ 278 | & (self.df['close'].shift(2) > self.df['close'].shift(5)) & (self.df['high'].shift(2) > self.df['high'].shift(7)) \ 279 | & (self.df['close'].shift(3) > self.df['close'].shift(6)) & (self.df['high'].shift(3) > self.df['high'].shift(8)) \ 280 | & (self.df['close'].shift(4) > self.df['close'].shift(7)) & (self.df['high'].shift(4) > self.df['high'].shift(9)) \ 281 | & (self.df['close'].shift(5) > self.df['close'].shift(8)) & (self.df['high'].shift(5) > self.df['high'].shift(10)) \ 282 | & (self.df['close'].shift(6) > self.df['close'].shift(9)) & (self.df['high'].shift(6) > self.df['high'].shift(11)) \ 283 | & (self.df['close'].shift(7) > self.df['close'].shift(10)) & (self.df['high'].shift(7) > self.df['high'].shift(12)) 284 | 285 | def addCandleAstralSell(self): 286 | self.df['astral_sell'] = self.candleAstralSell() 287 | 288 | def changePct(self): 289 | """Close change percentage""" 290 | 291 | close_pc = self.df['close'] / self.df['close'].shift(1) - 1 292 | close_pc = close_pc.fillna(0) 293 | return close_pc 294 | 295 | def addChangePct(self): 296 | """Adds the close percentage to the DataFrame""" 297 | 298 | self.df['close_pc'] = self.changePct() 299 | 300 | # cumulative returns 301 | self.df['close_cpc'] = (1 + self.df['close_pc']).cumprod() 302 | 303 | def cumulativeMovingAverage(self): 304 | """Calculates the Cumulative Moving Average (CMA)""" 305 | 306 | return self.df.close.expanding().mean() 307 | 308 | def addCMA(self): 309 | """Adds the Cumulative Moving Average (CMA) to the DataFrame""" 310 | 311 | self.df['cma'] = self.cumulativeMovingAverage() 312 | 313 | def exponentialMovingAverage(self, period): 314 | """Calculates the Exponential Moving Average (EMA)""" 315 | 316 | if not isinstance(period, int): 317 | raise TypeError('Period parameter is not perioderic.') 318 | 319 | if period < 5 or period > 200: 320 | raise ValueError('Period is out of range') 321 | 322 | if len(self.df) < period: 323 | raise Exception('Data range too small.') 324 | 325 | return self.df.close.ewm(span=period, adjust=False).mean() 326 | 327 | def addEMA(self, period): 328 | """Adds the Exponential Moving Average (EMA) the DateFrame""" 329 | 330 | if not isinstance(period, int): 331 | raise TypeError('Period parameter is not perioderic.') 332 | 333 | if period < 5 or period > 200: 334 | raise ValueError('Period is out of range') 335 | 336 | if len(self.df) < period: 337 | raise Exception('Data range too small.') 338 | 339 | self.df['ema' + str(period)] = self.exponentialMovingAverage(period) 340 | 341 | def calculateRelativeStrengthIndex(self, series, interval=14): 342 | """Calculates the RSI on a Pandas series of closing prices.""" 343 | 344 | if not isinstance(series, pd.Series): 345 | raise TypeError('Pandas Series required.') 346 | 347 | if not isinstance(interval, int): 348 | raise TypeError('Interval integer required.') 349 | 350 | if(len(series) < interval): 351 | raise IndexError('Pandas Series smaller than interval.') 352 | 353 | diff = series.diff(1).dropna() 354 | 355 | sum_gains = 0 * diff 356 | sum_gains[diff > 0] = diff[diff > 0] 357 | avg_gains = sum_gains.ewm(com=interval-1, min_periods=interval).mean() 358 | 359 | sum_losses = 0 * diff 360 | sum_losses[diff < 0] = diff[diff < 0] 361 | avg_losses = sum_losses.ewm(com=interval-1, min_periods=interval).mean() 362 | 363 | rs = abs(avg_gains / avg_losses) 364 | rsi = 100 - 100 / (1 + rs) 365 | 366 | return rsi 367 | 368 | def addFibonacciBollingerBands(self, interval=20, multiplier=3): 369 | """Adds Fibonacci Bollinger Bands.""" 370 | 371 | if not isinstance(interval, int): 372 | raise TypeError('Interval integer required.') 373 | 374 | if not isinstance(multiplier, int): 375 | raise TypeError('Multiplier integer required.') 376 | 377 | tp = (self.df['high'] + self.df['low'] + self.df['close']) / 3 378 | sma = tp.rolling(interval).mean() 379 | sd = multiplier * tp.rolling(interval).std() 380 | 381 | sma = sma.fillna(0) 382 | sd = sd.fillna(0) 383 | 384 | self.df['fbb_mid'] = sma 385 | self.df['fbb_upper0_236'] = sma + (0.236 * sd) 386 | self.df['fbb_upper0_382'] = sma + (0.382 * sd) 387 | self.df['fbb_upper0_5'] = sma + (0.5 * sd) 388 | self.df['fbb_upper0_618'] = sma + (0.618 * sd) 389 | self.df['fbb_upper0_764'] = sma + (0.764 * sd) 390 | self.df['fbb_upper1'] = sma + (1 * sd) 391 | self.df['fbb_lower0_236'] = sma - (0.236 * sd) 392 | self.df['fbb_lower0_382'] = sma - (0.382 * sd) 393 | self.df['fbb_lower0_5'] = sma - (0.5 * sd) 394 | self.df['fbb_lower0_618'] = sma - (0.618 * sd) 395 | self.df['fbb_lower0_764'] = sma - (0.764 * sd) 396 | self.df['fbb_lower1'] = sma - (1 * sd) 397 | 398 | def movingAverageConvergenceDivergence(self): 399 | """Calculates the Moving Average Convergence Divergence (MACD)""" 400 | 401 | if len(self.df) < 26: 402 | raise Exception('Data range too small.') 403 | 404 | if not self.df['ema12'].dtype == 'float64' and not self.df['ema12'].dtype == 'int64': 405 | raise AttributeError("Pandas DataFrame 'ema12' column not int64 or float64.") 406 | 407 | if not self.df['ema26'].dtype == 'float64' and not self.df['ema26'].dtype == 'int64': 408 | raise AttributeError("Pandas DataFrame 'ema26' column not int64 or float64.") 409 | 410 | df = pd.DataFrame() 411 | df['macd'] = self.df['ema12'] - self.df['ema26'] 412 | df['signal'] = df['macd'].ewm(span=9, adjust=False).mean() 413 | return df 414 | 415 | def addMACD(self): 416 | """Adds the Moving Average Convergence Divergence (MACD) to the DataFrame""" 417 | 418 | df = self.movingAverageConvergenceDivergence() 419 | self.df['macd'] = df['macd'] 420 | self.df['signal'] = df['signal'] 421 | 422 | def onBalanceVolume(self): 423 | """Calculate On-Balance Volume (OBV)""" 424 | 425 | return np.where(self.df['close'] == self.df['close'].shift(1), 0, np.where(self.df['close'] > self.df['close'].shift(1), self.df['volume'], 426 | np.where(self.df['close'] < self.df['close'].shift(1), -self.df['volume'], self.df.iloc[0]['volume']))).cumsum() 427 | 428 | def addOBV(self): 429 | """Add the On-Balance Volume (OBV) to the DataFrame""" 430 | 431 | self.df['obv'] = self.onBalanceVolume() 432 | self.df['obv_pc'] = self.df['obv'].pct_change() * 100 433 | self.df['obv_pc'] = np.round(self.df['obv_pc'].fillna(0), 2) 434 | 435 | def relativeStrengthIndex(self, period): 436 | """Calculate the Relative Strength Index (RSI)""" 437 | 438 | if not isinstance(period, int): 439 | raise TypeError('Period parameter is not perioderic.') 440 | 441 | if period < 7 or period > 21: 442 | raise ValueError('Period is out of range') 443 | 444 | # calculate relative strength index 445 | rsi = self.calculateRelativeStrengthIndex(self.df['close'], period) 446 | # default to midway-50 for first entries 447 | rsi = rsi.fillna(50) 448 | return rsi 449 | 450 | def addRSI(self, period): 451 | """Adds the Relative Strength Index (RSI) to the DataFrame""" 452 | 453 | if not isinstance(period, int): 454 | raise TypeError('Period parameter is not perioderic.') 455 | 456 | if period < 7 or period > 21: 457 | raise ValueError('Period is out of range') 458 | 459 | self.df['rsi' + str(period)] = self.relativeStrengthIndex(period) 460 | self.df['rsi' + str(period)] = self.df['rsi' + str(period)].replace(np.nan, 50) 461 | 462 | def seasonalARIMAModel(self): 463 | """Returns the Seasonal ARIMA Model for price predictions""" 464 | 465 | # parameters for SARIMAX 466 | model = SARIMAX(self.df['close'], trend='n', order=(0,1,0), seasonal_order=(1,1,1,12)) 467 | return model.fit(disp=-1) 468 | 469 | def seasonalARIMAModelFittedValues(self): 470 | """Returns the Seasonal ARIMA Model for price predictions""" 471 | 472 | return self.seasonalARIMAModel().fittedvalues 473 | 474 | def simpleMovingAverage(self, period): 475 | """Calculates the Simple Moving Average (SMA)""" 476 | 477 | if not isinstance(period, int): 478 | raise TypeError('Period parameter is not perioderic.') 479 | 480 | if period < 5 or period > 200: 481 | raise ValueError('Period is out of range') 482 | 483 | if len(self.df) < period: 484 | raise Exception('Data range too small.') 485 | 486 | return self.df.close.rolling(period, min_periods=1).mean() 487 | 488 | def addSMA(self, period): 489 | """Add the Simple Moving Average (SMA) to the DataFrame""" 490 | 491 | if not isinstance(period, int): 492 | raise TypeError('Period parameter is not perioderic.') 493 | 494 | if period < 5 or period > 200: 495 | raise ValueError('Period is out of range') 496 | 497 | if len(self.df) < period: 498 | raise Exception('Data range too small.') 499 | 500 | self.df['sma' + str(period)] = self.simpleMovingAverage(period) 501 | 502 | def addGoldenCross(self): 503 | """Add Golden Cross SMA50 over SMA200""" 504 | 505 | if 'sma50' not in self.df: 506 | self.addSMA(50) 507 | 508 | if 'sma200' not in self.df: 509 | self.addSMA(200) 510 | 511 | self.df['goldencross'] = self.df['sma50'] > self.df['sma200'] 512 | 513 | def addDeathCross(self): 514 | """Add Death Cross SMA50 over SMA200""" 515 | 516 | if 'sma50' not in self.df: 517 | self.addSMA(50) 518 | 519 | if 'sma200' not in self.df: 520 | self.addSMA(200) 521 | 522 | self.df['deathcross'] = self.df['sma50'] < self.df['sma200'] 523 | 524 | def addElderRayIndex(self): 525 | """Add Elder Ray Index""" 526 | 527 | if 'ema13' not in self.df: 528 | self.addEMA(13) 529 | 530 | self.df['elder_ray_bull'] = self.df['high'] - self.df['ema13'] 531 | self.df['elder_ray_bear'] = self.df['low'] - self.df['ema13'] 532 | 533 | # bear power’s value is negative but increasing (i.e. becoming less bearish) 534 | # bull power’s value is increasing (i.e. becoming more bullish) 535 | self.df['eri_buy'] = ((self.df['elder_ray_bear'] < 0) & (self.df['elder_ray_bear'] > self.df['elder_ray_bear'].shift(1))) | ((self.df['elder_ray_bull'] > self.df['elder_ray_bull'].shift(1))) 536 | 537 | # bull power’s value is positive but decreasing (i.e. becoming less bullish) 538 | # bear power’s value is decreasing (i.e., becoming more bearish) 539 | self.df['eri_sell'] = ((self.df['elder_ray_bull'] > 0) & (self.df['elder_ray_bull'] < self.df['elder_ray_bull'].shift(1))) | ((self.df['elder_ray_bull'] < self.df['elder_ray_bull'].shift(1))) 540 | 541 | def getSupportResistanceLevels(self): 542 | """Calculate the Support and Resistance Levels""" 543 | 544 | self.levels = [] 545 | self.__calculateSupportResistenceLevels() 546 | levels_ts = {} 547 | for level in self.levels: 548 | levels_ts[self.df.index[level[0]]] = level[1] 549 | # add the support levels to the DataFrame 550 | return pd.Series(levels_ts) 551 | 552 | def printSupportResistanceLevel(self, price=0): 553 | if isinstance(price, int) or isinstance(price, float): 554 | df = self.getSupportResistanceLevels() 555 | 556 | if len(df) > 0: 557 | df_last = df.tail(1) 558 | if float(df_last[0]) < price: 559 | print (' Support level of ' + str(df_last[0]) + ' formed at ' + str(df_last.index[0]), "\n") 560 | elif float(df_last[0]) > price: 561 | print (' Resistance level of ' + str(df_last[0]) + ' formed at ' + str(df_last.index[0]), "\n") 562 | else: 563 | print (' Support/Resistance level of ' + str(df_last[0]) + ' formed at ' + str(df_last.index[0]), "\n") 564 | 565 | def addEMABuySignals(self): 566 | """Adds the EMA12/EMA26 buy and sell signals to the DataFrame""" 567 | 568 | if not isinstance(self.df, pd.DataFrame): 569 | raise TypeError('Pandas DataFrame required.') 570 | 571 | if not 'close' in self.df.columns: 572 | raise AttributeError("Pandas DataFrame 'close' column required.") 573 | 574 | if not self.df['close'].dtype == 'float64' and not self.df['close'].dtype == 'int64': 575 | raise AttributeError( 576 | "Pandas DataFrame 'close' column not int64 or float64.") 577 | 578 | if not 'ema12' or not 'ema26' in self.df.columns: 579 | self.addEMA(12) 580 | self.addEMA(26) 581 | 582 | # true if EMA12 is above the EMA26 583 | self.df['ema12gtema26'] = self.df.ema12 > self.df.ema26 584 | # true if the current frame is where EMA12 crosses over above 585 | self.df['ema12gtema26co'] = self.df.ema12gtema26.ne(self.df.ema12gtema26.shift()) 586 | self.df.loc[self.df['ema12gtema26'] == False, 'ema12gtema26co'] = False 587 | 588 | # true if the EMA12 is below the EMA26 589 | self.df['ema12ltema26'] = self.df.ema12 < self.df.ema26 590 | # true if the current frame is where EMA12 crosses over below 591 | self.df['ema12ltema26co'] = self.df.ema12ltema26.ne(self.df.ema12ltema26.shift()) 592 | self.df.loc[self.df['ema12ltema26'] == False, 'ema12ltema26co'] = False 593 | 594 | def addSMABuySignals(self): 595 | """Adds the SMA50/SMA200 buy and sell signals to the DataFrame""" 596 | 597 | if not isinstance(self.df, pd.DataFrame): 598 | raise TypeError('Pandas DataFrame required.') 599 | 600 | if not 'close' in self.df.columns: 601 | raise AttributeError("Pandas DataFrame 'close' column required.") 602 | 603 | if not self.df['close'].dtype == 'float64' and not self.df['close'].dtype == 'int64': 604 | raise AttributeError( 605 | "Pandas DataFrame 'close' column not int64 or float64.") 606 | 607 | if not 'sma50' or not 'sma200' in self.df.columns: 608 | self.addSMA(50) 609 | self.addSMA(200) 610 | 611 | # true if SMA50 is above the SMA200 612 | self.df['sma50gtsma200'] = self.df.sma50 > self.df.sma200 613 | # true if the current frame is where SMA50 crosses over above 614 | self.df['sma50gtsma200co'] = self.df.sma50gtsma200.ne(self.df.sma50gtsma200.shift()) 615 | self.df.loc[self.df['sma50gtsma200'] == False, 'sma50gtsma200co'] = False 616 | 617 | # true if the SMA50 is below the SMA200 618 | self.df['sma50ltsma200'] = self.df.sma50 < self.df.sma200 619 | # true if the current frame is where SMA50 crosses over below 620 | self.df['sma50ltsma200co'] = self.df.sma50ltsma200.ne(self.df.sma50ltsma200.shift()) 621 | self.df.loc[self.df['sma50ltsma200'] == False, 'sma50ltsma200co'] = False 622 | 623 | def addMACDBuySignals(self): 624 | """Adds the MACD/Signal buy and sell signals to the DataFrame""" 625 | 626 | if not isinstance(self.df, pd.DataFrame): 627 | raise TypeError('Pandas DataFrame required.') 628 | 629 | if not 'close' in self.df.columns: 630 | raise AttributeError("Pandas DataFrame 'close' column required.") 631 | 632 | if not self.df['close'].dtype == 'float64' and not self.df['close'].dtype == 'int64': 633 | raise AttributeError("Pandas DataFrame 'close' column not int64 or float64.") 634 | 635 | if not 'macd' or not 'signal' in self.df.columns: 636 | self.addMACD() 637 | self.addOBV() 638 | 639 | # true if MACD is above the Signal 640 | self.df['macdgtsignal'] = self.df.macd > self.df.signal 641 | # true if the current frame is where MACD crosses over above 642 | self.df['macdgtsignalco'] = self.df.macdgtsignal.ne(self.df.macdgtsignal.shift()) 643 | self.df.loc[self.df['macdgtsignal'] == False, 'macdgtsignalco'] = False 644 | 645 | # true if the MACD is below the Signal 646 | self.df['macdltsignal'] = self.df.macd < self.df.signal 647 | # true if the current frame is where MACD crosses over below 648 | self.df['macdltsignalco'] = self.df.macdltsignal.ne(self.df.macdltsignal.shift()) 649 | self.df.loc[self.df['macdltsignal'] == False, 'macdltsignalco'] = False 650 | 651 | def getFibonacciRetracementLevels(self, price=0): 652 | # validates price is numeric 653 | if not isinstance(price, int) and not isinstance(price, float): 654 | raise TypeError('Optional price is not numeric.') 655 | 656 | price_min = self.df.close.min() 657 | price_max = self.df.close.max() 658 | 659 | diff = price_max - price_min 660 | 661 | data = {} 662 | 663 | if price != 0 and (price <= price_min): 664 | data['ratio1'] = float(self.__truncate(price_min, 2)) 665 | elif price == 0: 666 | data['ratio1'] = float(self.__truncate(price_min, 2)) 667 | 668 | if price != 0 and (price > price_min) and (price <= (price_max - 0.768 * diff)): 669 | data['ratio1'] = float(self.__truncate(price_min, 2)) 670 | data['ratio0_768'] = float(self.__truncate(price_max - 0.768 * diff, 2)) 671 | elif price == 0: 672 | data['ratio0_768'] = float(self.__truncate(price_max - 0.768 * diff, 2)) 673 | 674 | if price != 0 and (price > (price_max - 0.768 * diff)) and (price <= (price_max - 0.618 * diff)): 675 | data['ratio0_768'] = float(self.__truncate(price_max - 0.768 * diff, 2)) 676 | data['ratio0_618'] = float(self.__truncate(price_max - 0.618 * diff, 2)) 677 | elif price == 0: 678 | data['ratio0_618'] = float(self.__truncate(price_max - 0.618 * diff, 2)) 679 | 680 | if price != 0 and (price > (price_max - 0.618 * diff)) and (price <= (price_max - 0.5 * diff)): 681 | data['ratio0_618'] = float(self.__truncate(price_max - 0.618 * diff, 2)) 682 | data['ratio0_5'] = float(self.__truncate(price_max - 0.5 * diff, 2)) 683 | elif price == 0: 684 | data['ratio0_5'] = float(self.__truncate(price_max - 0.5 * diff, 2)) 685 | 686 | if price != 0 and (price > (price_max - 0.5 * diff)) and (price <= (price_max - 0.382 * diff)): 687 | data['ratio0_5'] = float(self.__truncate(price_max - 0.5 * diff, 2)) 688 | data['ratio0_382'] = float(self.__truncate(price_max - 0.382 * diff, 2)) 689 | elif price == 0: 690 | data['ratio0_382'] = float(self.__truncate(price_max - 0.382 * diff, 2)) 691 | 692 | if price != 0 and (price > (price_max - 0.382 * diff)) and (price <= (price_max - 0.286 * diff)): 693 | data['ratio0_382'] = float(self.__truncate(price_max - 0.382 * diff, 2)) 694 | data['ratio0_286'] = float(self.__truncate(price_max - 0.286 * diff, 2)) 695 | elif price == 0: 696 | data['ratio0_286'] = float(self.__truncate(price_max - 0.286 * diff, 2)) 697 | 698 | if price != 0 and (price > (price_max - 0.286 * diff)) and (price <= price_max): 699 | data['ratio0_286'] = float(self.__truncate(price_max - 0.286 * diff, 2)) 700 | data['ratio0'] = float(self.__truncate(price_max, 2)) 701 | elif price == 0: 702 | data['ratio0'] = float(self.__truncate(price_max, 2)) 703 | 704 | if price != 0 and (price < (price_max + 0.272 * diff)) and (price >= price_max): 705 | data['ratio0'] = float(self.__truncate(price_max, 2)) 706 | data['ratio1_272'] = float(self.__truncate(price_max + 0.272 * diff, 2)) 707 | elif price == 0: 708 | data['ratio1_272'] = float(self.__truncate(price_max + 0.272 * diff, 2)) 709 | 710 | if price != 0 and (price < (price_max + 0.414 * diff)) and (price >= (price_max + 0.272 * diff)): 711 | data['ratio1_272'] = float(self.__truncate(price_max, 2)) 712 | data['ratio1_414'] = float(self.__truncate(price_max + 0.414 * diff, 2)) 713 | elif price == 0: 714 | data['ratio1_414'] = float(self.__truncate(price_max + 0.414 * diff, 2)) 715 | 716 | if price != 0 and (price < (price_max + 0.618 * diff)) and (price >= (price_max + 0.414 * diff)): 717 | data['ratio1_618'] = float(self.__truncate(price_max + 0.618 * diff, 2)) 718 | elif price == 0: 719 | data['ratio1_618'] = float(self.__truncate(price_max + 0.618 * diff, 2)) 720 | 721 | return data 722 | 723 | def saveCSV(self, filename='tradingdata.csv'): 724 | """Saves the DataFrame to an uncompressed CSV.""" 725 | 726 | p = re.compile(r"^[\w\-. ]+$") 727 | if not p.match(filename): 728 | raise TypeError('Filename required.') 729 | 730 | if not isinstance(self.df, pd.DataFrame): 731 | raise TypeError('Pandas DataFrame required.') 732 | 733 | try: 734 | self.df.to_csv(filename) 735 | except OSError: 736 | print('Unable to save: ', filename) 737 | 738 | def __calculateSupportResistenceLevels(self): 739 | """Support and Resistance levels. (private function)""" 740 | 741 | for i in range(2, self.df.shape[0] - 2): 742 | if self.__isSupport(self.df, i): 743 | l = self.df['low'][i] 744 | if self.__isFarFromLevel(l): 745 | self.levels.append((i, l)) 746 | elif self.__isResistance(self.df, i): 747 | l = self.df['high'][i] 748 | if self.__isFarFromLevel(l): 749 | self.levels.append((i, l)) 750 | return self.levels 751 | 752 | def __isSupport(self, df, i): 753 | """Is support level? (privte function)""" 754 | 755 | c1 = df['low'][i] < df['low'][i - 1] 756 | c2 = df['low'][i] < df['low'][i + 1] 757 | c3 = df['low'][i + 1] < df['low'][i + 2] 758 | c4 = df['low'][i - 1] < df['low'][i - 2] 759 | support = c1 and c2 and c3 and c4 760 | return support 761 | 762 | def __isResistance(self, df, i): 763 | """Is resistance level? (private function)""" 764 | 765 | c1 = df['high'][i] > df['high'][i - 1] 766 | c2 = df['high'][i] > df['high'][i + 1] 767 | c3 = df['high'][i + 1] > df['high'][i + 2] 768 | c4 = df['high'][i - 1] > df['high'][i - 2] 769 | resistance = c1 and c2 and c3 and c4 770 | return resistance 771 | 772 | def __isFarFromLevel(self, l): 773 | """Is far from support level? (private function)""" 774 | 775 | s = np.mean(self.df['high'] - self.df['low']) 776 | return np.sum([abs(l-x) < s for x in self.levels]) == 0 777 | 778 | def __truncate(self, f, n): 779 | return math.floor(f * 10 ** n) / 10 ** n -------------------------------------------------------------------------------- /models/TradingAccount.py: -------------------------------------------------------------------------------- 1 | """Live or test trading account""" 2 | 3 | import sys 4 | import numpy as np 5 | import pandas as pd 6 | import json, math, re, requests, sys 7 | from datetime import datetime 8 | from binance.client import Client 9 | from models.Binance import AuthAPI as BAuthAPI, PublicAPI as BPublicAPI 10 | from models.CoinbasePro import AuthAPI as CBAuthAPI, PublicAPI as CBPublicAPI 11 | 12 | class TradingAccount(): 13 | def __init__(self, app={}): 14 | """Trading account object model 15 | 16 | Parameters 17 | ---------- 18 | app : object 19 | PyCryptoBot object 20 | """ 21 | 22 | # config needs to be a dictionary, empty or otherwise 23 | if not isinstance(app, object): 24 | raise TypeError('App is not a PyCryptoBot object.') 25 | 26 | if app.getExchange() == 'binance': 27 | self.client = Client(app.getAPIKey(), app.getAPISecret(), { 'verify': False, 'timeout': 20 }) 28 | 29 | # if trading account is for testing it will be instantiated with a balance of 1000 30 | self.balance = pd.DataFrame([ 31 | [ 'QUOTE', 1000, 0, 1000 ], 32 | [ 'BASE', 0, 0, 0 ]], 33 | columns=['currency','balance','hold','available']) 34 | 35 | self.app = app 36 | 37 | if app.isLive() == 1: 38 | self.mode = 'live' 39 | else: 40 | self.mode = 'test' 41 | 42 | self.orders = pd.DataFrame() 43 | 44 | def __convertStatus(self, val): 45 | if val == 'filled': 46 | return 'done' 47 | else: 48 | return val 49 | 50 | def _checkMarketSyntax(self, market): 51 | """Check that the market is syntactically correct 52 | 53 | Parameters 54 | ---------- 55 | market : str 56 | market to check 57 | """ 58 | if self.app.getExchange() == 'coinbasepro' and market != '': 59 | p = re.compile(r"^[1-9A-Z]{2,5}\-[1-9A-Z]{2,5}$") 60 | if not p.match(market): 61 | raise TypeError('Coinbase Pro market is invalid.') 62 | elif self.app.getExchange() == 'binance': 63 | p = re.compile(r"^[A-Z]{6,12}$") 64 | if not p.match(market): 65 | raise TypeError('Binance market is invalid.') 66 | 67 | def getOrders(self, market='', action='', status='all'): 68 | """Retrieves orders either live or simulation 69 | 70 | Parameters 71 | ---------- 72 | market : str, optional 73 | Filters orders by market 74 | action : str, optional 75 | Filters orders by action 76 | status : str 77 | Filters orders by status, defaults to 'all' 78 | """ 79 | 80 | # validate market is syntactically correct 81 | self._checkMarketSyntax(market) 82 | 83 | if action != '': 84 | # validate action is either a buy or sell 85 | if not action in ['buy', 'sell']: 86 | raise ValueError('Invalid order action.') 87 | 88 | # validate status is open, pending, done, active or all 89 | if not status in ['open', 'pending', 'done', 'active', 'all', 'filled']: 90 | raise ValueError('Invalid order status.') 91 | 92 | if self.app.getExchange() == 'binance': 93 | if self.mode == 'live': 94 | resp = self.client.get_all_orders(symbol=market) 95 | if len(resp) > 0: 96 | df = pd.DataFrame(resp) 97 | else: 98 | df = pd.DataFrame() 99 | 100 | if len(df) == 0: 101 | return pd.DataFrame() 102 | 103 | df = df[[ 'time', 'symbol', 'side', 'type', 'executedQty', 'cummulativeQuoteQty', 'status' ]] 104 | df.columns = [ 'created_at', 'market', 'action', 'type', 'size', 'value', 'status' ] 105 | df['created_at'] = df['created_at'].apply(lambda x: int(str(x)[:10])) 106 | df['created_at'] = df['created_at'].astype("datetime64[s]") 107 | df['size'] = df['size'].astype(float) 108 | df['value'] = df['value'].astype(float) 109 | df['action'] = df['action'].str.lower() 110 | df['type'] = df['type'].str.lower() 111 | df['status'] = df['status'].str.lower() 112 | df['price'] = df['value'] / df['size'] 113 | 114 | # pylint: disable=unused-variable 115 | for k, v in df.items(): 116 | if k == 'status': 117 | df[k] = df[k].map(self.__convertStatus) 118 | 119 | if action != '': 120 | df = df[df['action'] == action] 121 | df = df.reset_index(drop=True) 122 | 123 | if status != 'all' and status != '': 124 | df = df[df['status'] == status] 125 | df = df.reset_index(drop=True) 126 | 127 | return df 128 | else: 129 | # return dummy orders 130 | if market == '': 131 | return self.orders 132 | else: 133 | if (len(self.orders) > 0): 134 | return self.orders[self.orders['market'] == market] 135 | else: 136 | return pd.DataFrame() 137 | if self.app.getExchange() == 'coinbasepro': 138 | if self.mode == 'live': 139 | # if config is provided and live connect to Coinbase Pro account portfolio 140 | model = CBAuthAPI(self.app.getAPIKey(), self.app.getAPISecret(), self.app.getAPIPassphrase(), self.app.getAPIURL()) 141 | # retrieve orders from live Coinbase Pro account portfolio 142 | self.orders = model.getOrders(market, action, status) 143 | return self.orders 144 | else: 145 | # return dummy orders 146 | if market == '': 147 | return self.orders 148 | else: 149 | return self.orders[self.orders['market'] == market] 150 | 151 | def getBalance(self, currency=''): 152 | """Retrieves balance either live or simulation 153 | 154 | Parameters 155 | ---------- 156 | currency: str, optional 157 | Filters orders by currency 158 | """ 159 | 160 | if self.app.getExchange() == 'binance': 161 | if self.mode == 'live': 162 | resp = self.client.get_account() 163 | if 'balances' in resp: 164 | df = pd.DataFrame(resp['balances']) 165 | df = df[(df['free'] != '0.00000000') & (df['free'] != '0.00')] 166 | df['free'] = df['free'].astype(float) 167 | df['locked'] = df['locked'].astype(float) 168 | df['balance'] = df['free'] - df['locked'] 169 | df.columns = [ 'currency', 'available', 'hold', 'balance' ] 170 | df = df[[ 'currency', 'balance', 'hold', 'available' ]] 171 | df = df.reset_index(drop=True) 172 | 173 | if currency == '': 174 | # retrieve all balances 175 | return df 176 | else: 177 | # retrieve balance of specified currency 178 | df_filtered = df[df['currency'] == currency]['available'] 179 | if len(df_filtered) == 0: 180 | # return nil balance if no positive balance was found 181 | return 0.0 182 | else: 183 | # return balance of specified currency (if positive) 184 | if currency in ['EUR','GBP','USD']: 185 | return float(self.app.truncate(float(df[df['currency'] == currency]['available'].values[0]), 2)) 186 | else: 187 | return float(self.app.truncate(float(df[df['currency'] == currency]['available'].values[0]), 4)) 188 | else: 189 | return 0.0 190 | else: 191 | # return dummy balances 192 | if currency == '': 193 | # retrieve all balances 194 | return self.balance 195 | else: 196 | if self.app.getExchange() == 'binance': 197 | self.balance = self.balance.replace('QUOTE', currency) 198 | else: 199 | # replace QUOTE and BASE placeholders 200 | if currency in ['EUR','GBP','USD']: 201 | self.balance = self.balance.replace('QUOTE', currency) 202 | else: 203 | self.balance = self.balance.replace('BASE', currency) 204 | 205 | if self.balance.currency[self.balance.currency.isin([currency])].empty == True: 206 | self.balance.loc[len(self.balance)] = [currency,0,0,0] 207 | 208 | # retrieve balance of specified currency 209 | df = self.balance 210 | df_filtered = df[df['currency'] == currency]['available'] 211 | 212 | if len(df_filtered) == 0: 213 | # return nil balance if no positive balance was found 214 | return 0.0 215 | else: 216 | # return balance of specified currency (if positive) 217 | if currency in ['EUR','GBP','USD']: 218 | return float(self.app.truncate(float(df[df['currency'] == currency]['available'].values[0]), 2)) 219 | else: 220 | return float(self.app.truncate(float(df[df['currency'] == currency]['available'].values[0]), 4)) 221 | 222 | else: 223 | if self.mode == 'live': 224 | # if config is provided and live connect to Coinbase Pro account portfolio 225 | model = CBAuthAPI(self.app.getAPIKey(), self.app.getAPISecret(), self.app.getAPIPassphrase(), self.app.getAPIURL()) 226 | if currency == '': 227 | # retrieve all balances 228 | return model.getAccounts()[['currency', 'balance', 'hold', 'available']] 229 | else: 230 | df = model.getAccounts() 231 | # retrieve balance of specified currency 232 | df_filtered = df[df['currency'] == currency]['available'] 233 | if len(df_filtered) == 0: 234 | # return nil balance if no positive balance was found 235 | return 0.0 236 | else: 237 | # return balance of specified currency (if positive) 238 | if currency in ['EUR','GBP','USD']: 239 | return float(self.app.truncate(float(df[df['currency'] == currency]['available'].values[0]), 2)) 240 | else: 241 | return float(self.app.truncate(float(df[df['currency'] == currency]['available'].values[0]), 4)) 242 | 243 | else: 244 | # return dummy balances 245 | 246 | if currency == '': 247 | # retrieve all balances 248 | return self.balance 249 | else: 250 | # replace QUOTE and BASE placeholders 251 | if currency in ['EUR','GBP','USD']: 252 | self.balance = self.balance.replace('QUOTE', currency) 253 | elif currency in ['BCH','BTC','ETH','LTC','XLM']: 254 | self.balance = self.balance.replace('BASE', currency) 255 | 256 | if self.balance.currency[self.balance.currency.isin([currency])].empty == True: 257 | self.balance.loc[len(self.balance)] = [currency,0,0,0] 258 | 259 | # retrieve balance of specified currency 260 | df = self.balance 261 | df_filtered = df[df['currency'] == currency]['available'] 262 | 263 | if len(df_filtered) == 0: 264 | # return nil balance if no positive balance was found 265 | return 0.0 266 | else: 267 | # return balance of specified currency (if positive) 268 | if currency in ['EUR','GBP','USD']: 269 | return float(self.app.truncate(float(df[df['currency'] == currency]['available'].values[0]), 2)) 270 | else: 271 | return float(self.app.truncate(float(df[df['currency'] == currency]['available'].values[0]), 4)) 272 | 273 | def saveTrackerCSV(self, market='', save_file='tracker.csv'): 274 | """Saves order tracker to CSV 275 | 276 | Parameters 277 | ---------- 278 | market : str, optional 279 | Filters orders by market 280 | save_file : str 281 | Output CSV file 282 | """ 283 | 284 | # validate market is syntactically correct 285 | self._checkMarketSyntax(market) 286 | 287 | if self.mode == 'live': 288 | if self.app.getExchange() == 'coinbasepro': 289 | # retrieve orders from live Coinbase Pro account portfolio 290 | df = self.getOrders(market, '', 'done') 291 | elif self.app.getExchange() == 'binance': 292 | # retrieve orders from live Binance account portfolio 293 | df = self.getOrders(market, '', 'done') 294 | else: 295 | df = pd.DataFrame() 296 | else: 297 | # return dummy orders 298 | if market == '': 299 | df = self.orders 300 | else: 301 | if 'market' in self.orders: 302 | df = self.orders[self.orders['market'] == market] 303 | else: 304 | df = pd.DataFrame() 305 | 306 | if list(df.keys()) != [ 'created_at', 'market', 'action', 'type', 'size', 'value', 'fees', 'price', 'status' ]: 307 | # no data, return early 308 | return False 309 | 310 | df_tracker = pd.DataFrame() 311 | 312 | last_action = '' 313 | for market in df['market'].sort_values().unique(): 314 | df_market = df[df['market'] == market] 315 | 316 | df_buy = pd.DataFrame() 317 | df_sell = pd.DataFrame() 318 | 319 | pair = 0 320 | # pylint: disable=unused-variable 321 | for index, row in df_market.iterrows(): 322 | if row['action'] == 'buy': 323 | pair = 1 324 | 325 | if pair == 1 and (row['action'] != last_action): 326 | if row['action'] == 'buy': 327 | df_buy = row 328 | elif row['action'] == 'sell': 329 | df_sell = row 330 | 331 | if row['action'] == 'sell' and len(df_buy) != 0: 332 | df_pair = pd.DataFrame([ 333 | [ 334 | df_sell['status'], 335 | df_buy['market'], 336 | df_buy['created_at'], 337 | df_buy['type'], 338 | df_buy['size'], 339 | df_buy['value'], 340 | df_buy['fees'], 341 | df_buy['price'], 342 | df_sell['created_at'], 343 | df_sell['type'], 344 | df_sell['size'], 345 | df_sell['value'], 346 | df_sell['fees'], 347 | df_sell['price'] 348 | ]], columns=[ 'status', 'market', 349 | 'buy_at', 'buy_type', 'buy_size', 'buy_value', 'buy_fees', 'buy_price', 350 | 'sell_at', 'sell_type', 'sell_size', 'sell_value', 'sell_fees', 'sell_price' 351 | ]) 352 | df_tracker = df_tracker.append(df_pair, ignore_index=True) 353 | pair = 0 354 | 355 | last_action = row['action'] 356 | 357 | if list(df_tracker.keys()) != [ 'status', 'market', 358 | 'buy_at', 'buy_type', 'buy_size', 'buy_value', 'buy_fees', 'buy_price', 359 | 'sell_at', 'sell_type', 'sell_size', 'sell_value', 'sell_fees', 'sell_price' ]: 360 | # no data, return early 361 | return False 362 | 363 | df_tracker['profit'] = np.subtract(np.subtract(df_tracker['sell_value'], df_tracker['buy_value']), np.add(df_tracker['buy_fees'], df_tracker['sell_fees'])) 364 | df_tracker['margin'] = np.multiply(np.true_divide(df_tracker['profit'], df_tracker['buy_value']), 100) 365 | df_sincebot = df_tracker[df_tracker['buy_at'] > '2021-02-1'] 366 | 367 | try: 368 | df_sincebot.to_csv(save_file, index=False) 369 | except OSError: 370 | raise SystemExit('Unable to save: ', save_file) 371 | 372 | def buy(self, cryptoMarket, fiatMarket, fiatAmount=0, manualPrice=0.00000000): 373 | """Places a buy order either live or simulation 374 | 375 | Parameters 376 | ---------- 377 | cryptoMarket: str 378 | Crypto market you wish to purchase 379 | fiatMarket, str 380 | QUOTE market funding the purchase 381 | fiatAmount, float 382 | QUOTE amount of crypto currency to purchase 383 | manualPrice, float 384 | Used for simulations specifying the live price to purchase 385 | """ 386 | 387 | # fiat funding amount must be an integer or float 388 | if not isinstance(fiatAmount, float) and not isinstance(fiatAmount, int): 389 | raise TypeError('QUOTE amount not numeric.') 390 | 391 | # fiat funding amount must be positive 392 | if fiatAmount <= 0: 393 | raise Exception('Invalid QUOTE amount.') 394 | 395 | if self.app.getExchange() == 'binance': 396 | # validate crypto market is syntactically correct 397 | p = re.compile(r"^[A-Z]{3,8}$") 398 | if not p.match(cryptoMarket): 399 | raise TypeError('Binance crypto market is invalid.') 400 | 401 | # validate fiat market is syntactically correct 402 | p = re.compile(r"^[A-Z]{3,8}$") 403 | if not p.match(fiatMarket): 404 | raise TypeError('Binance fiat market is invalid.') 405 | else: 406 | # crypto market should be either BCH, BTC, ETH, LTC or XLM 407 | if cryptoMarket not in ['BCH', 'BTC', 'ETH', 'LTC', 'XLM']: 408 | raise Exception('Invalid crypto market: BCH, BTC, ETH, LTC, ETH, or XLM') 409 | 410 | # fiat market should be either EUR, GBP, or USD 411 | if fiatMarket not in ['EUR', 'GBP', 'USD']: 412 | raise Exception('Invalid QUOTE market: EUR, GBP, USD') 413 | 414 | # reconstruct the exchange market using crypto and fiat inputs 415 | if self.app.getExchange() == 'binance': 416 | market = cryptoMarket + fiatMarket 417 | else: 418 | market = cryptoMarket + '-' + fiatMarket 419 | 420 | if self.app.getExchange() == 'binance': 421 | if self.mode == 'live': 422 | # execute a live market buy 423 | resp = self.client.order_market_buy(symbol=market, quantity=fiatAmount) 424 | 425 | # TODO: not finished 426 | print(resp) 427 | else: 428 | # fiat amount should exceed balance 429 | if fiatAmount > self.getBalance(fiatMarket): 430 | raise Exception('Insufficient funds.') 431 | 432 | # manual price must be an integer or float 433 | if not isinstance(manualPrice, float) and not isinstance(manualPrice, int): 434 | raise TypeError('Optional manual price not numeric.') 435 | 436 | price = manualPrice 437 | # if manualPrice is non-positive retrieve the current live price 438 | if manualPrice <= 0: 439 | if self.app.getExchange() == 'binance': 440 | api = BPublicAPI() 441 | price = api.getTicker(market) 442 | else: 443 | resp = requests.get('https://api-public.sandbox.pro.coinbase.com/products/' + market + '/ticker') 444 | if resp.status_code != 200: 445 | raise Exception('GET /products/' + market + 446 | '/ticker {}'.format(resp.status_code)) 447 | resp.raise_for_status() 448 | json = resp.json() 449 | price = float(json['price']) 450 | 451 | # calculate purchase fees 452 | fee = fiatAmount * 0.005 453 | fiatAmountMinusFee = fiatAmount - fee 454 | total = float(fiatAmountMinusFee / float(price)) 455 | 456 | # append dummy order into orders dataframe 457 | ts = pd.Timestamp.now() 458 | price = (fiatAmountMinusFee * 100) / (total * 100) 459 | order = pd.DataFrame([['', market, 'buy', 'market', float('{:.8f}'.format(total)), fiatAmountMinusFee, 'done', '{:.8f}'.format(float(price))]], columns=[ 460 | 'created_at', 'market', 'action', 'type', 'size', 'value', 'status', 'price'], index=[ts]) 461 | order['created_at'] = order.index 462 | self.orders = pd.concat([self.orders, pd.DataFrame(order)], ignore_index=False) 463 | 464 | # update the dummy fiat balance 465 | self.balance.loc[self.balance['currency'] == fiatMarket, 'balance'] = self.getBalance(fiatMarket) - fiatAmount 466 | self.balance.loc[self.balance['currency'] == fiatMarket, 'available'] = self.getBalance(fiatMarket) - fiatAmount 467 | 468 | # update the dummy crypto balance 469 | self.balance.loc[self.balance['currency'] == cryptoMarket, 'balance'] = self.getBalance(cryptoMarket) + (fiatAmountMinusFee / price) 470 | self.balance.loc[self.balance['currency'] == cryptoMarket, 'available'] = self.getBalance(cryptoMarket) + (fiatAmountMinusFee / price) 471 | 472 | else: 473 | if self.mode == 'live': 474 | # connect to coinbase pro api (authenticated) 475 | model = CBAuthAPI(self.app.getAPIKey(), self.app.getAPISecret(), self.app.getAPIPassphrase(), self.app.getAPIURL()) 476 | 477 | # execute a live market buy 478 | if fiatAmount > 0: 479 | resp = model.marketBuy(market, fiatAmount) 480 | else: 481 | resp = model.marketBuy(market, float(self.getBalance(fiatMarket))) 482 | 483 | # TODO: not finished 484 | print(resp) 485 | else: 486 | # fiat amount should exceed balance 487 | if fiatAmount > self.getBalance(fiatMarket): 488 | raise Exception('Insufficient funds.') 489 | 490 | # manual price must be an integer or float 491 | if not isinstance(manualPrice, float) and not isinstance(manualPrice, int): 492 | raise TypeError('Optional manual price not numeric.') 493 | 494 | price = manualPrice 495 | # if manualPrice is non-positive retrieve the current live price 496 | if manualPrice <= 0: 497 | resp = requests.get('https://api-public.sandbox.pro.coinbase.com/products/' + market + '/ticker') 498 | if resp.status_code != 200: 499 | raise Exception('GET /products/' + market + 500 | '/ticker {}'.format(resp.status_code)) 501 | resp.raise_for_status() 502 | json = resp.json() 503 | price = float(json['price']) 504 | 505 | # calculate purchase fees 506 | fee = fiatAmount * 0.005 507 | fiatAmountMinusFee = fiatAmount - fee 508 | total = float(fiatAmountMinusFee / price) 509 | 510 | # append dummy order into orders dataframe 511 | ts = pd.Timestamp.now() 512 | price = (fiatAmountMinusFee * 100) / (total * 100) 513 | order = pd.DataFrame([['', market, 'buy', 'market', float('{:.8f}'.format(total)), fiatAmountMinusFee, 'done', price]], columns=[ 514 | 'created_at', 'market', 'action', 'type', 'size', 'value', 'status', 'price'], index=[ts]) 515 | order['created_at'] = order.index 516 | self.orders = pd.concat([self.orders, pd.DataFrame(order)], ignore_index=False) 517 | 518 | # update the dummy fiat balance 519 | self.balance.loc[self.balance['currency'] == fiatMarket, 'balance'] = self.getBalance(fiatMarket) - fiatAmount 520 | self.balance.loc[self.balance['currency'] == fiatMarket, 'available'] = self.getBalance(fiatMarket) - fiatAmount 521 | 522 | # update the dummy crypto balance 523 | self.balance.loc[self.balance['currency'] == cryptoMarket, 'balance'] = self.getBalance(cryptoMarket) + (fiatAmountMinusFee / price) 524 | self.balance.loc[self.balance['currency'] == cryptoMarket, 'available'] = self.getBalance(cryptoMarket) + (fiatAmountMinusFee / price) 525 | 526 | def sell(self, cryptoMarket, fiatMarket, cryptoAmount, manualPrice=0.00000000): 527 | """Places a sell order either live or simulation 528 | 529 | Parameters 530 | ---------- 531 | cryptoMarket: str 532 | Crypto market you wish to purchase 533 | fiatMarket, str 534 | QUOTE market funding the purchase 535 | fiatAmount, float 536 | QUOTE amount of crypto currency to purchase 537 | manualPrice, float 538 | Used for simulations specifying the live price to purchase 539 | """ 540 | if self.app.getExchange() == 'binance': 541 | # validate crypto market is syntactically correct 542 | p = re.compile(r"^[A-Z]{3,8}$") 543 | if not p.match(cryptoMarket): 544 | raise TypeError('Binance crypto market is invalid.') 545 | 546 | # validate fiat market is syntactically correct 547 | p = re.compile(r"^[A-Z]{3,8}$") 548 | if not p.match(fiatMarket): 549 | raise TypeError('Binance fiat market is invalid.') 550 | else: 551 | # crypto market should be either BCH, BTC, ETH, LTC or XLM 552 | if cryptoMarket not in ['BCH', 'BTC', 'ETH', 'LTC', 'XLM']: 553 | raise Exception('Invalid crypto market: BCH, BTC, ETH, LTC, ETH, or XLM') 554 | 555 | # fiat market should be either EUR, GBP, or USD 556 | if fiatMarket not in ['EUR', 'GBP', 'USD']: 557 | raise Exception('Invalid QUOTE market: EUR, GBP, USD') 558 | 559 | # reconstruct the exchange market using crypto and fiat inputs 560 | if self.app.getExchange() == 'binance': 561 | market = cryptoMarket + fiatMarket 562 | else: 563 | market = cryptoMarket + '-' + fiatMarket 564 | 565 | # crypto amount must be an integer or float 566 | if not isinstance(cryptoAmount, float) and not isinstance(cryptoAmount, int): 567 | raise TypeError('Crypto amount not numeric.') 568 | 569 | # crypto amount must be positive 570 | if cryptoAmount <= 0: 571 | raise Exception('Invalid crypto amount.') 572 | 573 | if self.app.getExchange() == 'binance': 574 | if self.mode == 'live': 575 | # execute a live market buy 576 | resp = self.client.order_market_sell(symbol=market, quantity=cryptoAmount) 577 | 578 | # TODO: not finished 579 | print(resp) 580 | else: 581 | # crypto amount should exceed balance 582 | if cryptoAmount > self.getBalance(cryptoMarket): 583 | raise Exception('Insufficient funds.') 584 | 585 | # manual price must be an integer or float 586 | if not isinstance(manualPrice, float) and not isinstance(manualPrice, int): 587 | raise TypeError('Optional manual price not numeric.') 588 | 589 | # calculate purchase fees 590 | fee = cryptoAmount * 0.005 591 | cryptoAmountMinusFee = cryptoAmount - fee 592 | 593 | price = manualPrice 594 | # if manualPrice is non-positive retrieve the current live price 595 | if manualPrice <= 0: 596 | resp = requests.get('https://api-public.sandbox.pro.coinbase.com/products/' + market + '/ticker') 597 | if resp.status_code != 200: 598 | raise Exception('GET /products/' + market + 599 | '/ticker {}'.format(resp.status_code)) 600 | resp.raise_for_status() 601 | json = resp.json() 602 | price = float(json['price']) 603 | 604 | total = price * cryptoAmountMinusFee 605 | 606 | # append dummy order into orders dataframe 607 | ts = pd.Timestamp.now() 608 | price = ((price * cryptoAmount) * 100) / (cryptoAmount * 100) 609 | order = pd.DataFrame([['', market, 'sell', 'market', cryptoAmountMinusFee, float('{:.8f}'.format( 610 | total)), 'done', '{:.8f}'.format(float(price))]], columns=['created_at', 'market', 'action', 'type', 'size', 'value', 'status', 'price'], index=[ts]) 611 | order['created_at'] = order.index 612 | self.orders = pd.concat([self.orders, pd.DataFrame(order)], ignore_index=False) 613 | 614 | # update the dummy fiat balance 615 | self.balance.loc[self.balance['currency'] == fiatMarket, 'balance'] = self.getBalance(fiatMarket) + total 616 | self.balance.loc[self.balance['currency'] == fiatMarket, 'available'] = self.getBalance(fiatMarket) + total 617 | 618 | # update the dummy crypto balance 619 | self.balance.loc[self.balance['currency'] == cryptoMarket, 'balance'] = self.getBalance(cryptoMarket) - cryptoAmount 620 | self.balance.loc[self.balance['currency'] == cryptoMarket, 'available'] = self.getBalance(cryptoMarket) - cryptoAmount 621 | 622 | else: 623 | if self.mode == 'live': 624 | # connect to Coinbase Pro API live 625 | model = CBAuthAPI(self.app.getAPIKey(), self.app.getAPISecret(), self.app.getAPIPassphrase(), self.app.getAPIURL()) 626 | 627 | # execute a live market sell 628 | resp = model.marketSell(market, float(self.getBalance(cryptoMarket))) 629 | 630 | # TODO: not finished 631 | print(resp) 632 | else: 633 | # crypto amount should exceed balance 634 | if cryptoAmount > self.getBalance(cryptoMarket): 635 | raise Exception('Insufficient funds.') 636 | 637 | # manual price must be an integer or float 638 | if not isinstance(manualPrice, float) and not isinstance(manualPrice, int): 639 | raise TypeError('Optional manual price not numeric.') 640 | 641 | # calculate purchase fees 642 | fee = cryptoAmount * 0.005 643 | cryptoAmountMinusFee = cryptoAmount - fee 644 | 645 | price = manualPrice 646 | if manualPrice <= 0: 647 | # if manualPrice is non-positive retrieve the current live price 648 | resp = requests.get('https://api-public.sandbox.pro.coinbase.com/products/' + market + '/ticker') 649 | if resp.status_code != 200: 650 | raise Exception('GET /products/' + market + '/ticker {}'.format(resp.status_code)) 651 | resp.raise_for_status() 652 | json = resp.json() 653 | price = float(json['price']) 654 | 655 | total = price * cryptoAmountMinusFee 656 | 657 | # append dummy order into orders dataframe 658 | ts = pd.Timestamp.now() 659 | price = ((price * cryptoAmount) * 100) / (cryptoAmount * 100) 660 | order = pd.DataFrame([[market, 'sell', 'market', cryptoAmountMinusFee, float('{:.8f}'.format( 661 | total)), 'done', price]], columns=['market', 'action', 'type', 'size', 'value', 'status', 'price'], index=[ts]) 662 | order['created_at'] = order.index 663 | self.orders = pd.concat([self.orders, pd.DataFrame(order)], ignore_index=False) 664 | 665 | # update the dummy fiat balance 666 | self.balance.loc[self.balance['currency'] == fiatMarket, 'balance'] = self.getBalance(fiatMarket) + total 667 | self.balance.loc[self.balance['currency'] == fiatMarket, 'available'] = self.getBalance(fiatMarket) + total 668 | 669 | # update the dummy crypto balance 670 | self.balance.loc[self.balance['currency'] == cryptoMarket, 'balance'] = self.getBalance(cryptoMarket) - cryptoAmount 671 | self.balance.loc[self.balance['currency'] == cryptoMarket, 'available'] = self.getBalance(cryptoMarket) - cryptoAmount -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rvn911/pycryptobot/b5bb0f54b56888788674f50740921cacf4025e3a/models/__init__.py -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -p no:warnings 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | pandas 3 | requests 4 | statsmodels 5 | matplotlib 6 | binance 7 | python-binance 8 | -------------------------------------------------------------------------------- /sandbox-tracker.py: -------------------------------------------------------------------------------- 1 | import json 2 | from models.TradingAccount import TradingAccount 3 | 4 | with open('config.json') as config_file: 5 | config = json.load(config_file) 6 | 7 | account = TradingAccount(config) 8 | account.saveTrackerCSV() -------------------------------------------------------------------------------- /script-get_orders.py: -------------------------------------------------------------------------------- 1 | from models.PyCryptoBot import PyCryptoBot 2 | from models.TradingAccount import TradingAccount 3 | 4 | # Coinbase Pro orders 5 | app = PyCryptoBot(exchange='coinbasepro') 6 | app.setLive(1) 7 | account = TradingAccount(app) 8 | orders = account.getOrders() 9 | print (orders) 10 | 11 | # Binance Live orders 12 | app = PyCryptoBot(exchange='binance') 13 | app.setLive(1) 14 | account = TradingAccount(app) 15 | orders = account.getOrders('DOGEBTC') 16 | print (orders) -------------------------------------------------------------------------------- /script-get_time.py: -------------------------------------------------------------------------------- 1 | from models.PyCryptoBot import PyCryptoBot 2 | from models.Binance import PublicAPI as BPublicAPI 3 | from models.CoinbasePro import PublicAPI as CBPublicAPI 4 | 5 | # Coinbase Pro time 6 | api = CBPublicAPI() 7 | ts = api.getTime() 8 | print (ts) 9 | 10 | app = PyCryptoBot(exchange='coinbasepro') 11 | ts = api.getTime() 12 | print (ts) 13 | 14 | # Binance Live time 15 | api = BPublicAPI() 16 | ts = api.getTime() 17 | print (ts) 18 | 19 | app = PyCryptoBot(exchange='binance') 20 | ts = api.getTime() 21 | print (ts) -------------------------------------------------------------------------------- /tests/test_Exchanges.py: -------------------------------------------------------------------------------- 1 | import pytest, json, sys 2 | import pandas as pd 3 | import urllib3 4 | 5 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 6 | 7 | sys.path.append('.') 8 | # pylint: disable=import-error 9 | from models.TradingAccount import TradingAccount 10 | 11 | BINANCE_MARKET_WITH_ORDERS = 'DOGEBTC' 12 | COINBASEPRO_MARKET_WITH_ORDERS = 'BTC-GBP' 13 | 14 | def test_default_instantiates_without_error(): 15 | account = TradingAccount() 16 | assert type(account) is TradingAccount 17 | assert account.getExchange() == 'coinbasepro' 18 | assert account.getMode() == 'test' 19 | 20 | def test_coinbasepro_instantiates_without_error(): 21 | config = { 22 | "api_url" : "https://api.pro.coinbase.com", 23 | "api_key" : "00000000000000000000000000000000", 24 | "api_secret" : "0000/0000000000/0000000000000000000000000000000000000000000000000000000000/00000000000==", 25 | "api_pass" : "00000000000" 26 | } 27 | 28 | account = TradingAccount(config) 29 | assert type(account) is TradingAccount 30 | assert account.getExchange() == 'coinbasepro' 31 | assert account.getMode() == 'live' 32 | 33 | def test_coinbasepro_api_url_error(): 34 | config = { 35 | "api_url" : "error", 36 | "api_key" : "00000000000000000000000000000000", 37 | "api_secret" : "0000/0000000000/0000000000000000000000000000000000000000000000000000000000/00000000000==", 38 | "api_pass" : "00000000000" 39 | } 40 | 41 | with pytest.raises(ValueError) as execinfo: 42 | TradingAccount(config) 43 | assert str(execinfo.value) == 'Coinbase Pro API URL is invalid' 44 | 45 | def test_coinbasepro_api_key_error(): 46 | config = { 47 | "api_url" : "https://api.pro.coinbase.com", 48 | "api_key" : "error", 49 | "api_secret" : "0000/0000000000/0000000000000000000000000000000000000000000000000000000000/00000000000==", 50 | "api_pass" : "00000000000" 51 | } 52 | 53 | with pytest.raises(TypeError) as execinfo: 54 | TradingAccount(config) 55 | assert str(execinfo.value) == 'Coinbase Pro API key is invalid' 56 | 57 | def test_coinbasepro_api_secret_error(): 58 | config = { 59 | "api_url" : "https://api.pro.coinbase.com", 60 | "api_key" : "00000000000000000000000000000000", 61 | "api_secret" : "error", 62 | "api_pass" : "00000000000" 63 | } 64 | 65 | with pytest.raises(TypeError) as execinfo: 66 | TradingAccount(config) 67 | assert str(execinfo.value) == 'Coinbase Pro API secret is invalid' 68 | 69 | def test_coinbasepro_api_pass_error(): 70 | config = { 71 | "api_url" : "https://api.pro.coinbase.com", 72 | "api_key" : "00000000000000000000000000000000", 73 | "api_secret" : "0000/0000000000/0000000000000000000000000000000000000000000000000000000000/00000000000==", 74 | "api_pass" : "error" 75 | } 76 | 77 | with pytest.raises(TypeError) as execinfo: 78 | TradingAccount(config) 79 | assert str(execinfo.value) == 'Coinbase Pro API passphrase is invalid' 80 | 81 | def test_binance_instantiates_without_error(): 82 | config = { 83 | "exchange" : "binance", 84 | "api_url" : "https://api.binance.com", 85 | "api_key" : "0000000000000000000000000000000000000000000000000000000000000000", 86 | "api_secret" : "0000000000000000000000000000000000000000000000000000000000000000" 87 | } 88 | 89 | account = TradingAccount(config) 90 | assert type(account) is TradingAccount 91 | assert account.getExchange() == 'binance' 92 | assert account.getMode() == 'live' 93 | 94 | def test_binance_api_url_error(): 95 | config = { 96 | "exchange" : "binance", 97 | "api_url" : "error", 98 | "api_key" : "0000000000000000000000000000000000000000000000000000000000000000", 99 | "api_secret" : "0000000000000000000000000000000000000000000000000000000000000000" 100 | } 101 | 102 | with pytest.raises(ValueError) as execinfo: 103 | TradingAccount(config) 104 | assert str(execinfo.value) == 'Binance API URL is invalid' 105 | 106 | def test_binance_api_key_error(): 107 | config = { 108 | "exchange" : "binance", 109 | "api_url" : "https://api.binance.com", 110 | "api_key" : "error", 111 | "api_secret" : "0000000000000000000000000000000000000000000000000000000000000000" 112 | } 113 | 114 | with pytest.raises(TypeError) as execinfo: 115 | TradingAccount(config) 116 | assert str(execinfo.value) == 'Binance API key is invalid' 117 | 118 | def test_binance_api_secret_error(): 119 | config = { 120 | "exchange" : "binance", 121 | "api_url" : "https://api.binance.com", 122 | "api_key" : "0000000000000000000000000000000000000000000000000000000000000000", 123 | "api_secret" : "error" 124 | } 125 | 126 | with pytest.raises(TypeError) as execinfo: 127 | TradingAccount(config) 128 | assert str(execinfo.value) == 'Binance API secret is invalid' 129 | 130 | def test_binance_not_load(): 131 | config = { 132 | "api_url" : "https://api.binance.com", 133 | "api_key" : "0000000000000000000000000000000000000000000000000000000000000000", 134 | "api_secret" : "0000000000000000000000000000000000000000000000000000000000000000" 135 | } 136 | 137 | account = TradingAccount(config) 138 | assert type(account) is TradingAccount 139 | assert account.getExchange() == 'coinbasepro' 140 | assert account.getMode() == 'test' 141 | 142 | def test_dummy_balances(): 143 | account = TradingAccount() 144 | actual = account.getBalance().columns.to_list() 145 | if len(actual) == 0: 146 | pytest.skip('No balances to perform test') 147 | assert type(actual) is list 148 | expected = [ 'currency', 'balance', 'hold', 'available' ] 149 | assert len(actual) == len(expected) 150 | assert all([a == b for a, b in zip(actual, expected)]) 151 | assert account.getExchange() == 'coinbasepro' 152 | assert account.getMode() == 'test' 153 | 154 | def test_dummy_market_balance(): 155 | account = TradingAccount() 156 | actual = account.getBalance('GBP') 157 | assert type(actual) is float 158 | assert account.getExchange() == 'coinbasepro' 159 | assert account.getMode() == 'test' 160 | 161 | def test_coinbasepro_balances(): 162 | try: 163 | with open('config-coinbasepro.json') as config_file: 164 | config = json.load(config_file) 165 | 166 | account = TradingAccount(config) 167 | assert type(account) is TradingAccount 168 | assert account.getExchange() == 'coinbasepro' 169 | assert account.getMode() == 'live' 170 | 171 | actual = account.getBalance().columns.to_list() 172 | if len(actual) == 0: 173 | pytest.skip('No balances to perform test') 174 | assert type(actual) is list 175 | expected = [ 'currency', 'balance', 'hold', 'available' ] 176 | assert len(actual) == len(expected) 177 | assert all([a == b for a, b in zip(actual, expected)]) 178 | 179 | except IOError: 180 | pytest.skip('config-coinbasepro.json does not exist to perform test') 181 | 182 | def test_coinbasepro_market_balance(): 183 | try: 184 | with open('config-coinbasepro.json') as config_file: 185 | config = json.load(config_file) 186 | 187 | account = TradingAccount(config) 188 | actual = account.getBalance('BTC') 189 | assert type(actual) is float 190 | assert account.getExchange() == 'coinbasepro' 191 | assert account.getMode() == 'live' 192 | 193 | except IOError: 194 | pytest.skip('config-coinbasepro.json does not exist to perform test') 195 | 196 | def test_binance_balances(): 197 | try: 198 | with open('config-binance.json') as config_file: 199 | config = json.load(config_file) 200 | 201 | account = TradingAccount(config) 202 | assert type(account) is TradingAccount 203 | assert account.getExchange() == 'binance' 204 | assert account.getMode() == 'live' 205 | 206 | actual = account.getBalance().columns.to_list() 207 | if len(actual) == 0: 208 | pytest.skip('No orders to perform test') 209 | assert type(actual) is list 210 | expected = [ 'currency', 'balance', 'hold', 'available' ] 211 | assert len(actual) == len(expected) 212 | assert all([a == b for a, b in zip(actual, expected)]) 213 | 214 | except IOError: 215 | pytest.skip('config-binance.json does not exist to perform test') 216 | 217 | def test_binance_market_balance(): 218 | try: 219 | with open('config-binance.json') as config_file: 220 | config = json.load(config_file) 221 | 222 | account = TradingAccount(config) 223 | actual = account.getBalance('BTC') 224 | assert type(actual) is float 225 | assert account.getExchange() == 'binance' 226 | assert account.getMode() == 'live' 227 | 228 | except IOError: 229 | pytest.skip('config-binance.json does not exist to perform test') 230 | 231 | def test_dummy_orders(): 232 | account = TradingAccount() 233 | account.buy('BTC', 'GBP', 1000, 30000) 234 | actual = account.getOrders().columns.to_list() 235 | if len(actual) == 0: 236 | pytest.skip('No orders to perform test') 237 | expected = [ 'created_at', 'market', 'action', 'type', 'size', 'value', 'status', 'price' ] 238 | assert len(actual) == len(expected) 239 | assert all([a == b for a, b in zip(actual, expected)]) 240 | assert account.getExchange() == 'coinbasepro' 241 | assert account.getMode() == 'test' 242 | 243 | def test_coinbasepro_all_orders(): 244 | try: 245 | with open('config-coinbasepro.json') as config_file: 246 | config = json.load(config_file) 247 | 248 | account = TradingAccount(config) 249 | assert type(account) is TradingAccount 250 | assert account.getExchange() == 'coinbasepro' 251 | assert account.getMode() == 'live' 252 | 253 | actual = account.getOrders().columns.to_list() 254 | if len(actual) == 0: 255 | pytest.skip('No orders to perform test') 256 | expected = [ 'created_at', 'market', 'action', 'type', 'size', 'value', 'status', 'price' ] 257 | assert len(actual) == len(expected) 258 | assert all([a == b for a, b in zip(actual, expected)]) 259 | 260 | except IOError: 261 | pytest.skip('config-coinbasepro.json does not exist to perform test') 262 | 263 | def test_coinbasepro_market_orders(): 264 | try: 265 | with open('config-coinbasepro.json') as config_file: 266 | config = json.load(config_file) 267 | 268 | account = TradingAccount(config) 269 | assert type(account) is TradingAccount 270 | assert account.getExchange() == 'coinbasepro' 271 | assert account.getMode() == 'live' 272 | 273 | actual = account.getOrders(COINBASEPRO_MARKET_WITH_ORDERS).columns.to_list() 274 | if len(actual) == 0: 275 | pytest.skip('No orders to perform test') 276 | expected = [ 'created_at', 'market', 'action', 'type', 'size', 'value', 'status', 'price' ] 277 | assert len(actual) == len(expected) 278 | assert all([a == b for a, b in zip(actual, expected)]) 279 | 280 | except IOError: 281 | pytest.skip('config-coinbasepro.json does not exist to perform test') 282 | 283 | def test_binance_market_orders(): 284 | try: 285 | with open('config-binance.json') as config_file: 286 | config = json.load(config_file) 287 | 288 | account = TradingAccount(config) 289 | assert type(account) is TradingAccount 290 | assert account.getExchange() == 'binance' 291 | assert account.getMode() == 'live' 292 | 293 | actual = account.getOrders(BINANCE_MARKET_WITH_ORDERS).columns.to_list() 294 | if len(actual) == 0: 295 | pytest.skip('No orders to perform test') 296 | expected = [ 'created_at', 'market', 'action', 'type', 'size', 'value', 'status', 'price' ] 297 | assert len(actual) == len(expected) 298 | assert all([a == b for a, b in zip(actual, expected)]) 299 | 300 | except IOError: 301 | pytest.skip('config-binance.json does not exist to perform test') 302 | 303 | def test_binance_market_buy_insufficient_funds(): 304 | try: 305 | with open('config-binance.json') as config_file: 306 | config = json.load(config_file) 307 | 308 | account = TradingAccount(config) 309 | with pytest.raises(Exception) as execinfo: 310 | account.buy('DOGE', 'BTC', 1000000, 0.000025) 311 | assert str(execinfo.value) == 'APIError(code=-2010): Account has insufficient balance for requested action.' 312 | 313 | except IOError: 314 | pytest.skip('config-binance.json does not exist to perform test') 315 | 316 | def test_coinbasepro_market_buy_insufficient_funds(): 317 | with open('config-coinbasepro.json') as config_file: 318 | config = json.load(config_file) 319 | 320 | account = TradingAccount(config) 321 | resp = account.buy('BTC', 'GBP', 20000) 322 | assert str(resp) == 'None' -------------------------------------------------------------------------------- /tests/test_TradingAccount.py: -------------------------------------------------------------------------------- 1 | import pytest, sys 2 | import pandas as pd 3 | 4 | sys.path.append('.') 5 | # pylint: disable=import-error 6 | from models.TradingAccount import TradingAccount 7 | 8 | def test_default_initial_balance(): 9 | account = TradingAccount() 10 | assert type(account.getBalance('GBP')) is float 11 | assert account.getBalance('GBP') == 1000 12 | 13 | def test_buy_sufficient_funds(): 14 | account = TradingAccount() 15 | account.buy('BTC', 'GBP', 1000, 30000) 16 | assert type(account.getBalance('GBP')) is float 17 | assert account.getBalance('GBP') == 0 18 | 19 | def test_buy_insufficient_funds(): 20 | account = TradingAccount() 21 | with pytest.raises(Exception) as execinfo: 22 | account.buy('BTC', 'GBP', 1001, 30000) 23 | assert str(execinfo.value) == 'Insufficient funds.' 24 | 25 | def test_sell_insufficient_funds(): 26 | account = TradingAccount() 27 | account.buy('BTC', 'GBP', 1000, 30000) 28 | with pytest.raises(Exception) as execinfo: 29 | account.sell('BTC', 'GBP', 1, 35000) 30 | assert str(execinfo.value) == 'Insufficient funds.' 31 | 32 | def test_successful_buy_and_sell(): 33 | account = TradingAccount() 34 | account.buy('BTC', 'GBP', 1000, 30000) 35 | account.sell('BTC', 'GBP', 0.0331, 35000) 36 | assert type(account.getBalance('GBP')) is float 37 | assert account.getBalance('GBP') == 1152.7 38 | 39 | def test_unspecified_balance_returns_dict(): 40 | account = TradingAccount() 41 | assert type(account.getBalance()) is pd.DataFrame 42 | 43 | def test_orders_returns_dict(): 44 | account = TradingAccount() 45 | assert type(account.getOrders()) is pd.DataFrame 46 | 47 | def test_orders_columns(): 48 | account = TradingAccount() 49 | account.buy('BTC', 'GBP', 1000, 30000) 50 | actual = account.getOrders().columns.to_list() 51 | expected = [ 'created_at', 'market', 'action', 'type', 'size', 'value', 'status', 'price' ] 52 | assert len(actual) == len(expected) 53 | assert all([a == b for a, b in zip(actual, expected)]) 54 | 55 | def test_orders_filtering(): 56 | account = TradingAccount() 57 | account.buy('BTC','GBP',250,30000) 58 | assert len(account.getOrders()) == 1 59 | account.sell('BTC','GBP',0.0082,35000) 60 | assert len(account.getOrders()) == 2 61 | account.buy('ETH','GBP',250,30000) 62 | assert len(account.getOrders()) == 3 63 | account.sell('ETH','GBP',0.0082,35000) 64 | assert len(account.getOrders()) == 4 65 | assert len(account.getOrders('BTC-GBP')) == 2 -------------------------------------------------------------------------------- /troubleshoot.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | from models.PyCryptoBot import PyCryptoBot 5 | from models.Trading import TechnicalAnalysis 6 | from models.Binance import AuthAPI as BAuthAPI, PublicAPI as BPublicAPI 7 | from models.CoinbasePro import AuthAPI as CBAuthAPI, PublicAPI as CBPublicAPI 8 | from views.TradingGraphs import TradingGraphs 9 | 10 | app = PyCryptoBot() 11 | trading_data = app.getHistoricalData(app.getMarket(), app.getGranularity()) 12 | 13 | ta = TechnicalAnalysis(trading_data) 14 | ta.addAll() 15 | 16 | df_data = ta.getDataFrame() 17 | df_fib = ta.getFibonacciRetracementLevels() 18 | df_sr = ta.getSupportResistanceLevels() 19 | 20 | print (df_data) 21 | print (df_fib) 22 | print (df_sr) 23 | 24 | graphs = TradingGraphs(ta) 25 | #graphs.renderBuySellSignalEMA1226MACD(saveOnly=False) 26 | #graphs = TradingGraphs(ta) 27 | #graphs.renderPercentageChangeHistogram() 28 | #graphs.renderCumulativeReturn() 29 | #graphs.renderPercentageChangeScatterMatrix() 30 | graphs.renderFibonacciBollingerBands(period=24) -------------------------------------------------------------------------------- /views/TradingGraphs.py: -------------------------------------------------------------------------------- 1 | """Plots (and/or saves) the graphical trading data using Matplotlib""" 2 | 3 | import pandas as pd 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | import matplotlib.ticker as ticker 7 | from models.Trading import TechnicalAnalysis 8 | import datetime, re, sys 9 | sys.path.append('.') 10 | 11 | class TradingGraphs(): 12 | def __init__(self, technical_analysis): 13 | """Trading Graphs object model 14 | 15 | Parameters 16 | ---------- 17 | technical_analysis : object 18 | TechnicalAnalysis object to provide the trading data to visualise 19 | """ 20 | 21 | # validates the technical_analysis object 22 | if not isinstance(technical_analysis, TechnicalAnalysis): 23 | raise TypeError('Coinbase Pro model required.') 24 | 25 | # only one figure can be open at a time, close all open figures 26 | plt.close('all') 27 | 28 | self.technical_analysis = technical_analysis 29 | 30 | # stores the pandas dataframe from technical_analysis object 31 | self.df = technical_analysis.getDataFrame() 32 | 33 | # stores the support and resistance levels from technical_analysis object 34 | self.levels = technical_analysis.getSupportResistanceLevels() 35 | 36 | # set graph format 37 | plt.style.use('seaborn') 38 | 39 | def renderBuySellSignalEMA1226(self, saveFile='', saveOnly=False): 40 | """Render the EMA12 and EMA26 buy and sell signals 41 | 42 | Parameters 43 | ---------- 44 | saveFile : str, optional 45 | Save the figure 46 | saveOnly : bool 47 | Save the figure without displaying it 48 | """ 49 | 50 | buysignals = self.df[self.df.ema12gtema26co == True] 51 | sellsignals = self.df[self.df.ema12ltema26co == True] 52 | 53 | plt.subplot(111) 54 | plt.plot(self.df.close, label="price", color="royalblue") 55 | plt.plot(self.df.ema12, label="ema12", color="orange") 56 | plt.plot(self.df.ema26, label="ema26", color="purple") 57 | plt.ylabel('Price') 58 | 59 | for idx in buysignals.index.tolist(): 60 | plt.axvline(x=idx, color='green') 61 | 62 | for idx in sellsignals.index.tolist(): 63 | plt.axvline(x=idx, color='red') 64 | 65 | plt.xticks(rotation=90) 66 | plt.tight_layout() 67 | plt.legend() 68 | 69 | try: 70 | if saveFile != '': 71 | plt.savefig(saveFile) 72 | except OSError: 73 | raise SystemExit('Unable to save: ', saveFile) 74 | 75 | if saveOnly == False: 76 | plt.show() 77 | 78 | def renderBuySellSignalEMA1226MACD(self, saveFile='', saveOnly=False): 79 | """Render the EMA12, EMA26 and MACD buy and sell signals 80 | 81 | Parameters 82 | ---------- 83 | saveFile : str, optional 84 | Save the figure 85 | saveOnly : bool 86 | Save the figure without displaying it 87 | """ 88 | 89 | buysignals = ((self.df.ema12gtema26co == True) & (self.df.macdgtsignal == True) & (self.df.goldencross == True)) 90 | sellsignals = ((self.df.ema12ltema26co == True) & (self.df.macdltsignal == True)) 91 | df_signals = self.df[(buysignals) | (sellsignals)] 92 | 93 | ax1 = plt.subplot(211) 94 | plt.plot(self.df.close, label="price", color="royalblue") 95 | plt.plot(self.df.ema12, label="ema12", color="orange") 96 | plt.plot(self.df.ema26, label="ema26", color="purple") 97 | plt.ylabel('Price') 98 | 99 | action = '' 100 | last_action = '' 101 | for idx, row in df_signals.iterrows(): 102 | if row['ema12gtema26co'] == True and row['macdgtsignal'] == True and last_action != 'buy': 103 | action = 'buy' 104 | plt.axvline(x=idx, color='green') 105 | elif row['ema12ltema26'] == True and row['macdltsignal'] == True and action == 'buy': 106 | action = 'sell' 107 | plt.axvline(x=idx, color='red') 108 | 109 | last_action = action 110 | 111 | plt.xticks(rotation=90) 112 | 113 | plt.subplot(212, sharex=ax1) 114 | plt.plot(self.df.macd, label="macd") 115 | plt.plot(self.df.signal, label="signal") 116 | plt.legend() 117 | plt.ylabel('Divergence') 118 | plt.xticks(rotation=90) 119 | 120 | plt.tight_layout() 121 | plt.legend() 122 | 123 | try: 124 | if saveFile != '': 125 | plt.savefig(saveFile) 126 | except OSError: 127 | raise SystemExit('Unable to save: ', saveFile) 128 | 129 | if saveOnly == False: 130 | plt.show() 131 | 132 | def renderFibonacciBollingerBands(self, period=50, saveFile='', saveOnly=False): 133 | """Render FibonacciBollingerBands""" 134 | 135 | if not isinstance(period, int): 136 | raise TypeError('Period parameter is not perioderic.') 137 | 138 | if period < 1 or period > len(self.df): 139 | raise ValueError('Period is out of range') 140 | 141 | df_subset = self.df.iloc[-period::] 142 | 143 | plt.subplot(111) 144 | plt.suptitle(df_subset.iloc[0]['market'] + ' | ' + str(df_subset.iloc[0]['granularity']), fontsize=12) 145 | plt.plot(df_subset.fbb_upper0_236, label="23.6%", color="blue") 146 | plt.plot(df_subset.fbb_lower0_236, label="-23.6%", color="blue") 147 | plt.plot(df_subset.fbb_upper0_382, label="38.2%", color="green") 148 | plt.plot(df_subset.fbb_lower0_382, label="3-8.2%", color="green") 149 | plt.plot(df_subset.fbb_upper0_5, label="50%", color="cyan") 150 | plt.plot(df_subset.fbb_lower0_5, label="-50%", color="cyan") 151 | plt.plot(df_subset.fbb_upper0_618, label="61.8%", color="pink") 152 | plt.plot(df_subset.fbb_lower0_618, label="-61.8%", color="pink") 153 | plt.plot(df_subset.fbb_upper0_764, label="76.4%", color="red") 154 | plt.plot(df_subset.fbb_lower0_764, label="-76.4%", color="red") 155 | plt.plot(df_subset.fbb_upper1, label="100%", color="magenta") 156 | plt.plot(df_subset.fbb_lower1, label="-100%", color="magenta") 157 | plt.plot(df_subset.fbb_mid, label="mid", color="orange") 158 | plt.plot(df_subset.close, label="price", color="black") 159 | plt.legend() 160 | plt.ylabel('Price') 161 | plt.xticks(rotation=90) 162 | plt.tight_layout() 163 | 164 | try: 165 | if saveFile != '': 166 | plt.savefig(saveFile) 167 | except OSError: 168 | raise SystemExit('Unable to save: ', saveFile) 169 | 170 | if saveOnly == False: 171 | plt.show() 172 | 173 | def renderPriceEMA12EMA26(self, saveFile='', saveOnly=False): 174 | """Render the price, EMA12 and EMA26 175 | 176 | Parameters 177 | ---------- 178 | saveFile : str, optional 179 | Save the figure 180 | saveOnly : bool 181 | Save the figure without displaying it 182 | """ 183 | 184 | plt.subplot(111) 185 | plt.plot(self.df.close, label="price") 186 | plt.plot(self.df.ema12, label="ema12") 187 | plt.plot(self.df.ema26, label="ema26") 188 | plt.legend() 189 | plt.ylabel('Price') 190 | plt.xticks(rotation=90) 191 | plt.tight_layout() 192 | 193 | try: 194 | if saveFile != '': 195 | plt.savefig(saveFile) 196 | except OSError: 197 | raise SystemExit('Unable to save: ', saveFile) 198 | 199 | if saveOnly == False: 200 | plt.show() 201 | 202 | def renderEMAandMACD(self, period=30, saveFile='', saveOnly=False): 203 | """Render the price, EMA12, EMA26 and MACD 204 | 205 | Parameters 206 | ---------- 207 | saveFile : str, optional 208 | Save the figure 209 | saveOnly : bool 210 | Save the figure without displaying it 211 | """ 212 | 213 | if not isinstance(period, int): 214 | raise TypeError('Period parameter is not perioderic.') 215 | 216 | if period < 1 or period > len(self.df): 217 | raise ValueError('Period is out of range') 218 | 219 | df_subset = self.df.iloc[-period::] 220 | 221 | date = pd.to_datetime(df_subset.index).to_pydatetime() 222 | 223 | df_subset_length = len(df_subset) 224 | indices = np.arange(df_subset_length) # the evenly spaced plot indices 225 | 226 | def format_date(x, pos=None): #pylint: disable=unused-argument 227 | thisind = np.clip(int(x + 0.5), 0, df_subset_length - 1) 228 | return date[thisind].strftime('%Y-%m-%d %H:%M:%S') 229 | 230 | fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(12, 6)) 231 | fig.suptitle(df_subset.iloc[0]['market'] + ' | ' + str(df_subset.iloc[0]['granularity']), fontsize=16) 232 | plt.xticks(rotation=90) 233 | #plt.tight_layout() 234 | 235 | indices = np.arange(len(df_subset)) 236 | 237 | ax1.plot(indices, df_subset['close'], label='price', color='royalblue') 238 | ax1.plot(indices, df_subset['ema12'], label='ema12', color='orange') 239 | ax1.plot(indices, df_subset['ema26'], label='ema26', color='purple') 240 | ax1.xaxis.set_major_formatter(ticker.FuncFormatter(format_date)) 241 | ax1.set_title('Price, EMA12 and EMA26') 242 | ax1.set_ylabel('Price') 243 | ax1.legend() 244 | fig.autofmt_xdate() 245 | 246 | ax2.plot(indices, df_subset.macd, label='macd') 247 | ax2.plot(indices, df_subset.signal, label='signal') 248 | ax2.xaxis.set_major_formatter(ticker.FuncFormatter(format_date)) 249 | ax2.set_title('MACD') 250 | ax2.set_ylabel('Divergence') 251 | ax2.legend() 252 | fig.autofmt_xdate() 253 | 254 | try: 255 | if saveFile != '': 256 | plt.savefig(saveFile) 257 | except OSError: 258 | raise SystemExit('Unable to save: ', saveFile) 259 | 260 | if saveOnly == False: 261 | plt.show() 262 | 263 | def renderSeasonalARIMAModel(self, saveFile='', saveOnly=False): 264 | """Render the seasonal ARIMA model 265 | 266 | Parameters 267 | ---------- 268 | saveFile : str, optional 269 | Save the figure 270 | saveOnly : bool 271 | Save the figure without displaying it 272 | """ 273 | 274 | fittedValues = self.technical_analysis.seasonalARIMAModelFittedValues() 275 | 276 | plt.plot(self.df['close'], label='original') 277 | plt.plot(fittedValues, color='red', label='fitted') 278 | plt.title('RSS: %.4f' % sum((fittedValues-self.df['close'])**2)) 279 | plt.legend() 280 | plt.ylabel('Price') 281 | plt.xticks(rotation=90) 282 | plt.tight_layout() 283 | 284 | try: 285 | if saveFile != '': 286 | plt.savefig(saveFile) 287 | except OSError: 288 | raise SystemExit('Unable to save: ', saveFile) 289 | 290 | if saveOnly == False: 291 | plt.show() 292 | 293 | def renderSMAandMACD(self, saveFile='', saveOnly=False): 294 | """Render the price, SMA20, SMA50, and SMA200 295 | 296 | Parameters 297 | ---------- 298 | saveFile : str, optional 299 | Save the figure 300 | saveOnly : bool 301 | Save the figure without displaying it 302 | """ 303 | 304 | ax1 = plt.subplot(211) 305 | plt.plot(self.df.close, label="price") 306 | plt.plot(self.df.sma20, label="sma20") 307 | plt.plot(self.df.sma50, label="sma50") 308 | plt.plot(self.df.sma200, label="sma200") 309 | plt.legend() 310 | plt.ylabel('Price') 311 | plt.xticks(rotation=90) 312 | plt.subplot(212, sharex=ax1) 313 | plt.plot(self.df.macd, label="macd") 314 | plt.plot(self.df.signal, label="signal") 315 | plt.legend() 316 | plt.ylabel('Price') 317 | plt.xlabel('Days') 318 | plt.xticks(rotation=90) 319 | plt.tight_layout() 320 | 321 | try: 322 | if saveFile != '': 323 | plt.savefig(saveFile) 324 | except OSError: 325 | raise SystemExit('Unable to save: ', saveFile) 326 | 327 | if saveOnly == False: 328 | plt.show() 329 | 330 | 331 | def renderSeasonalARIMAModelPrediction(self, days=30, saveOnly=False): 332 | """Render the seasonal ARIMA model prediction 333 | 334 | Parameters 335 | ---------- 336 | days : int 337 | Number of days to predict 338 | saveOnly : bool 339 | Save the figure without displaying it 340 | """ 341 | 342 | # get dataframe from technical analysis object 343 | df = self.technical_analysis.getDataFrame() 344 | 345 | if not isinstance(days, int): 346 | raise TypeError('Prediction days is not numeric.') 347 | 348 | if days < 1 or days > len(df): 349 | raise ValueError('Predication days is out of range') 350 | 351 | # extract market and granularity from trading dataframe 352 | market = df.iloc[0].market 353 | granularity = df.iloc[0].granularity 354 | 355 | results_ARIMA = self.technical_analysis.seasonalARIMAModel() 356 | 357 | df = pd.DataFrame(self.df['close']) 358 | start_date = df.last_valid_index() 359 | end_date = start_date + datetime.timedelta(days=days) 360 | pred = results_ARIMA.predict(start=str(start_date), end=str(end_date), dynamic=True) 361 | 362 | fig, axes = plt.subplots(ncols=1, figsize=(12, 6)) #pylint: disable=unused-variable 363 | fig.autofmt_xdate() 364 | ax1 = plt.subplot(111) 365 | ax1.set_title('Seasonal ARIMA Model Prediction') 366 | 367 | date = pd.to_datetime(pred.index).to_pydatetime() 368 | 369 | pred_length = len(pred) 370 | # the evenly spaced plot indices 371 | indices = np.arange(pred_length) #pylint: disable=unused-variable 372 | 373 | def format_date(x, pos=None): #pylint: disable=unused-argument 374 | thisind = np.clip(int(x + 0.5), 0, pred_length - 1) 375 | return date[thisind].strftime('%Y-%m-%d %H:%M:%S') 376 | 377 | fig, ax = plt.subplots(ncols=1, figsize=(12, 6)) #pylint: disable=unused-variable 378 | fig.autofmt_xdate() 379 | 380 | ax = plt.subplot(111) 381 | ax.set_title('Seasonal ARIMA Model Prediction') 382 | ax.plot(pred, label='prediction', color='black') 383 | ax.xaxis.set_major_formatter(ticker.FuncFormatter(format_date)) 384 | 385 | plt.xticks(rotation=90) 386 | plt.tight_layout() 387 | 388 | try: 389 | print ('creating: graphs/SAM_' + market + '_' + str(granularity) + '.png') 390 | plt.savefig('graphs/SAM_' + market + '_' + str(granularity) + '.png', dpi=300) 391 | except OSError: 392 | raise SystemExit('Unable to save: graphs/SAM_' + market + '_' + str(granularity) + '.png') 393 | 394 | if saveOnly == False: 395 | plt.show() 396 | 397 | def renderCandlestickAstralPattern(self, period=30, saveOnly=False): 398 | # get dataframe from technical analysis object 399 | df = self.technical_analysis.getDataFrame() 400 | 401 | if not isinstance(period, int): 402 | raise TypeError('Period parameter is not perioderic.') 403 | 404 | if period < 1 or period > len(df): 405 | raise ValueError('Period is out of range') 406 | 407 | # extract market and granularity from trading dataframe 408 | market = df.iloc[0].market 409 | granularity = df.iloc[0].granularity 410 | 411 | df_subset = df.iloc[-period::] 412 | 413 | fig, axes = plt.subplots(ncols=1, figsize=(12, 6)) #pylint: disable=unused-variable 414 | fig.autofmt_xdate() 415 | ax1 = plt.subplot(111) 416 | ax1.set_title('Astral Candlestick Pattern') 417 | plt.plot(df_subset['close'], label='price', color='black') 418 | plt.plot(df_subset['ema12'], label='ema12', color='orange') 419 | plt.plot(df_subset['ema26'], label='ema26', color='purple') 420 | plt.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) 421 | 422 | df_candlestick = self.df[self.df['astral_buy'] == True] 423 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 424 | for idx in df_candlestick_in_range.index.tolist(): 425 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'g^', markersize=8) 426 | 427 | df_candlestick = self.df[self.df['astral_sell'] == True] 428 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 429 | for idx in df_candlestick_in_range.index.tolist(): 430 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'rv', markersize=8) 431 | 432 | plt.xlabel(market + ' - ' + str(granularity)) 433 | plt.ylabel('Price') 434 | plt.xticks(rotation=90) 435 | plt.tight_layout() 436 | plt.legend() 437 | 438 | try: 439 | print ('creating: graphs/CAP_' + market + '_' + str(granularity) + '.png') 440 | plt.savefig('graphs/CAP_' + market + '_' + str(granularity) + '.png', dpi=300) 441 | except OSError: 442 | raise SystemExit('Unable to save: graphs/CAP_' + market + '_' + str(granularity) + '.png') 443 | 444 | if saveOnly == False: 445 | plt.show() 446 | 447 | def renderCandlesticks(self, period=30, saveOnly=False): 448 | # get dataframe from technical analysis object 449 | df = self.technical_analysis.getDataFrame() 450 | 451 | if not isinstance(period, int): 452 | raise TypeError('Period parameter is not perioderic.') 453 | 454 | if period < 1 or period > len(df): 455 | raise ValueError('Period is out of range') 456 | 457 | # extract market and granularity from trading dataframe 458 | market = df.iloc[0].market 459 | granularity = df.iloc[0].granularity 460 | 461 | df_subset = df.iloc[-period::] 462 | 463 | fig, axes = plt.subplots(ncols=1, figsize=(12, 6)) #pylint: disable=unused-variable 464 | fig.autofmt_xdate() 465 | ax1 = plt.subplot(111) 466 | ax1.set_title('Candlestick Patterns') 467 | plt.plot(df_subset['close'], label='price', color='black') 468 | plt.plot(df_subset['ema12'], label='ema12', color='orange') 469 | plt.plot(df_subset['ema26'], label='ema26', color='purple') 470 | plt.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) 471 | 472 | df_candlestick = self.df[self.df['three_white_soldiers'] == True] 473 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 474 | for idx in df_candlestick_in_range.index.tolist(): 475 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'g*', markersize=10, label='Three White Soldiers') 476 | 477 | df_candlestick = self.df[self.df['three_black_crows'] == True] 478 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 479 | for idx in df_candlestick_in_range.index.tolist(): 480 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'r*', markersize=10, label='Three Black Crows') 481 | 482 | df_candlestick = self.df[self.df['inverted_hammer'] == True] 483 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 484 | for idx in df_candlestick_in_range.index.tolist(): 485 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'g*', markersize=10, label='Inverted Hammer') 486 | 487 | df_candlestick = self.df[self.df['hammer'] == True] 488 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 489 | for idx in df_candlestick_in_range.index.tolist(): 490 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'g*', markersize=10, label='Hammer') 491 | 492 | df_candlestick = self.df[self.df['hanging_man'] == True] 493 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 494 | for idx in df_candlestick_in_range.index.tolist(): 495 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'r*', markersize=10, label='Hanging Man') 496 | 497 | df_candlestick = self.df[self.df['shooting_star'] == True] 498 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 499 | for idx in df_candlestick_in_range.index.tolist(): 500 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'r*', markersize=10, label='Shooting Star') 501 | 502 | df_candlestick = self.df[self.df['doji'] == True] 503 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 504 | for idx in df_candlestick_in_range.index.tolist(): 505 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'b*', markersize=10, label='Doji') 506 | 507 | df_candlestick = self.df[self.df['three_line_strike'] == True] 508 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 509 | for idx in df_candlestick_in_range.index.tolist(): 510 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'g*', markersize=10, label='Three Line Strike') 511 | 512 | df_candlestick = self.df[self.df['two_black_gapping'] == True] 513 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 514 | for idx in df_candlestick_in_range.index.tolist(): 515 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'r*', markersize=10, label='Two Black Gapping') 516 | 517 | df_candlestick = self.df[self.df['morning_star'] == True] 518 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 519 | for idx in df_candlestick_in_range.index.tolist(): 520 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'g*', markersize=10, label='Morning Star') 521 | 522 | df_candlestick = self.df[self.df['evening_star'] == True] 523 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 524 | for idx in df_candlestick_in_range.index.tolist(): 525 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'r*', markersize=10, label='Evening Star') 526 | 527 | df_candlestick = self.df[self.df['morning_doji_star'] == True] 528 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 529 | for idx in df_candlestick_in_range.index.tolist(): 530 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'g*', markersize=10, label='Morning Doji Star') 531 | 532 | df_candlestick = self.df[self.df['evening_doji_star'] == True] 533 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 534 | for idx in df_candlestick_in_range.index.tolist(): 535 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'r*', markersize=10, label='Evening Doji Star') 536 | 537 | df_candlestick = self.df[self.df['abandoned_baby'] == True] 538 | df_candlestick_in_range = df_candlestick[df_candlestick.index >= np.min(df_subset.index)] 539 | for idx in df_candlestick_in_range.index.tolist(): 540 | plt.plot(idx, df_candlestick_in_range.loc[idx]['close'], 'g*', markersize=10, label='Abandoned Baby') 541 | 542 | plt.xlabel(market + ' - ' + str(granularity)) 543 | plt.ylabel('Price') 544 | plt.xticks(rotation=90) 545 | plt.tight_layout() 546 | plt.legend() 547 | 548 | try: 549 | print ('creating: graphs/CSP_' + market + '_' + str(granularity) + '.png') 550 | plt.savefig('graphs/CSP_' + market + '_' + str(granularity) + '.png', dpi=300) 551 | except OSError: 552 | raise SystemExit('Unable to save: graphs/CSP_' + market + '_' + str(granularity) + '.png') 553 | 554 | if saveOnly == False: 555 | plt.show() 556 | 557 | def renderFibonacciRetracement(self, saveOnly=False): 558 | """Render Fibonacci Retracement Levels 559 | 560 | Parameters 561 | ---------- 562 | saveOnly : bool 563 | Save the figure without displaying it 564 | """ 565 | 566 | # get dataframe from technical analysis object 567 | df = self.technical_analysis.getDataFrame() 568 | 569 | # extract market and granularity from trading dataframe 570 | market = df.iloc[0].market 571 | granularity = df.iloc[0].granularity 572 | 573 | # closing price min and max values 574 | price_min = df.close.min() 575 | price_max = df.close.max() 576 | 577 | # fibonacci retracement levels 578 | diff = price_max - price_min 579 | level1 = price_max - 0.236 * diff 580 | level2 = price_max - 0.382 * diff 581 | level3 = price_max - 0.618 * diff 582 | 583 | fig, ax = plt.subplots(ncols=1, figsize=(12, 6)) #pylint: disable=unused-variable 584 | fig.autofmt_xdate() 585 | 586 | ax = plt.subplot(111) 587 | ax.plot(df.close, label='price', color='black') 588 | ax.set_title('Fibonacci Retracement Levels') 589 | ax.axhspan(level1, price_min, alpha=0.4, color='lightsalmon', label='0.618') 590 | ax.axhspan(level3, level2, alpha=0.5, color='palegreen', label='0.382') 591 | ax.axhspan(level2, level1, alpha=0.5, color='palegoldenrod', label='0.236') 592 | ax.axhspan(price_max, level3, alpha=0.5, color='powderblue', label='0') 593 | 594 | plt.xlabel(market + ' - ' + str(granularity)) 595 | plt.ylabel('Price') 596 | plt.xticks(rotation=90) 597 | plt.tight_layout() 598 | plt.legend() 599 | 600 | try: 601 | print ('creating: graphs/FRL_' + market + '_' + str(granularity) + '.png') 602 | plt.savefig('graphs/FRL_' + market + '_' + str(granularity) + '.png', dpi=300) 603 | except OSError: 604 | raise SystemExit('Unable to save: graphs/FRL_' + market + '_' + str(granularity) + '.png') 605 | 606 | if saveOnly == False: 607 | plt.show() 608 | 609 | def renderSupportResistance(self, saveOnly=False): 610 | """Render Support and Resistance Levels 611 | 612 | Parameters 613 | ---------- 614 | saveOnly : bool 615 | Save the figure without displaying it 616 | """ 617 | 618 | # get dataframe from technical analysis object 619 | df = self.technical_analysis.getDataFrame() 620 | 621 | # extract market and granularity from trading dataframe 622 | market = df.iloc[0].market 623 | granularity = df.iloc[0].granularity 624 | 625 | fig, ax = plt.subplots(ncols=1, figsize=(12, 6)) #pylint: disable=unused-variable 626 | fig.autofmt_xdate() 627 | 628 | ax = plt.subplot(111) 629 | ax.plot(df.close, label='price', color='black') 630 | ax.set_title('Support and Resistance Levels') 631 | 632 | rotation = 1 633 | last_level = 0 634 | for level in self.levels: 635 | #plt.axhline(y=level, color='grey') 636 | if last_level != 0: 637 | if rotation == 1: 638 | ax.axhspan(last_level, level, alpha=0.4, color='lightsalmon', label=str(level)) 639 | elif rotation == 2: 640 | ax.axhspan(last_level, level, alpha=0.5, color='palegreen', label=str(level)) 641 | elif rotation == 3: 642 | ax.axhspan(last_level, level, alpha=0.5, color='palegoldenrod', label=str(level)) 643 | elif rotation == 4: 644 | ax.axhspan(last_level, level, alpha=0.5, color='powderblue', label=str(level)) 645 | else: 646 | ax.axhspan(last_level, level, alpha=0.4) 647 | last_level = level 648 | if rotation < 4: 649 | rotation += 1 650 | else: 651 | rotation = 1 652 | 653 | plt.xlabel(market + ' - ' + str(granularity)) 654 | plt.ylabel('Price') 655 | plt.xticks(rotation=90) 656 | plt.tight_layout() 657 | plt.legend() 658 | 659 | try: 660 | print ('creating: graphs/SRL_' + market + '_' + str(granularity) + '.png') 661 | plt.savefig('graphs/SRL_' + market + '_' + str(granularity) + '.png', dpi=300) 662 | except OSError: 663 | raise SystemExit('Unable to save: graphs/SRL_' + market + '_' + str(granularity) + '.png') 664 | 665 | if saveOnly == False: 666 | plt.show() 667 | 668 | def renderPercentageChangeHistogram(self, show_desc=True): 669 | """Render Percentage Change Histogram 670 | 671 | Parameters 672 | ---------- 673 | saveOnly : bool 674 | Save the figure without displaying it 675 | """ 676 | 677 | # get dataframe from technical analysis object 678 | df = self.technical_analysis.getDataFrame() 679 | 680 | # extract market and granularity from trading dataframe 681 | market = df.iloc[0].market 682 | granularity = df.iloc[0].granularity 683 | 684 | fig, ax = plt.subplots(ncols=1, figsize=(12, 6)) #pylint: disable=unused-variable 685 | fig.autofmt_xdate() 686 | 687 | ax = plt.subplot(111) 688 | df.close_pc.hist(bins=50) 689 | ax.set_title('Close Percent Change') 690 | 691 | plt.xlabel(market + ' - ' + str(granularity)) 692 | plt.xticks(rotation=90) 693 | plt.tight_layout() 694 | plt.legend() 695 | 696 | plt.show() 697 | 698 | if show_desc == True: 699 | print(df['close_pc'].describe()) 700 | 701 | def renderPercentageChangeScatterMatrix(self): 702 | """Render Percentage Change Scatter Matrix 703 | 704 | Parameters 705 | ---------- 706 | saveOnly : bool 707 | Save the figure without displaying it 708 | """ 709 | 710 | # get dataframe from technical analysis object 711 | df = self.technical_analysis.getDataFrame() 712 | 713 | pd.plotting.scatter_matrix(df[['close','close_pc','close_cpc']], diagonal='kde', alpha=0.1, figsize=(12,12)) 714 | plt.tight_layout() 715 | plt.show() 716 | 717 | def renderCumulativeReturn(self): 718 | """Render Percentage Change Histogram 719 | 720 | Parameters 721 | ---------- 722 | saveOnly : bool 723 | Save the figure without displaying it 724 | """ 725 | 726 | # get dataframe from technical analysis object 727 | df = self.technical_analysis.getDataFrame() 728 | 729 | # extract market and granularity from trading dataframe 730 | market = df.iloc[0].market 731 | granularity = df.iloc[0].granularity 732 | 733 | fig, ax = plt.subplots(ncols=1, figsize=(12, 6)) #pylint: disable=unused-variable 734 | fig.autofmt_xdate() 735 | 736 | ax = plt.subplot(111) 737 | ax.plot(df.close_cpc, label='Adj Close', color='black') 738 | ax.set_title('Cumulative Return') 739 | 740 | plt.xlabel(market + ' - ' + str(granularity)) 741 | plt.ylabel('Return') 742 | plt.xticks(rotation=90) 743 | plt.tight_layout() 744 | plt.legend() 745 | 746 | plt.show() -------------------------------------------------------------------------------- /views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rvn911/pycryptobot/b5bb0f54b56888788674f50740921cacf4025e3a/views/__init__.py --------------------------------------------------------------------------------