├── .dockerignore ├── .github └── workflows │ ├── docker.yml │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── SUMMARY.md ├── ai-example.py ├── dev_requirements.txt ├── docs ├── data │ └── fetching_data.md ├── plotting │ └── basis.md ├── strategies │ └── basis.md └── trading │ └── orders.md ├── example.py ├── octobot_script ├── __init__.py ├── ai │ ├── __init__.py │ ├── agents.py │ ├── environments.py │ └── models.py ├── api │ ├── __init__.py │ ├── data_fetching.py │ ├── execution.py │ └── ploting.py ├── cli.py ├── config │ ├── config_mock.json │ └── logging_config.ini ├── constants.py ├── internal │ ├── __init__.py │ ├── backtester_trading_mode.py │ ├── logging_util.py │ ├── octobot_mocks.py │ └── runners.py ├── model │ ├── __init__.py │ ├── backtest_plot.py │ ├── backtest_result.py │ ├── errors.py │ └── strategy.py └── resources │ ├── __init__.py │ └── reports │ ├── css │ ├── style.css │ └── w2ui_template.css │ ├── default_report_template.html │ ├── header.html │ ├── js │ ├── common.js │ ├── data.js │ ├── graphs.js │ ├── tables.js │ └── texts.js │ └── scripts.html ├── requirements-ai.txt ├── requirements.txt ├── setup.py ├── standard.rc ├── start.py └── tests ├── __init__.py ├── api ├── __init__.py ├── test_data_fetching.py └── test_execution.py ├── functionnal ├── __init__.py └── example_scripts │ ├── __init__.py │ └── test_precomputed_vs_iteration_rsi.py └── test_util └── ExchangeHistoryDataCollector_1673796151.325921.data /.dockerignore: -------------------------------------------------------------------------------- 1 | # dev 2 | .idea 3 | 4 | # CI files 5 | .coveragerc 6 | .coveralls.yml 7 | .travis.yml 8 | appveyor.yml 9 | renovate.json 10 | setup.cfg 11 | tox.ini 12 | 13 | # octobot 14 | tentacles 15 | user 16 | logs 17 | 18 | # Git 19 | .git 20 | Dockerfile 21 | .DS_Store 22 | .gitignore 23 | .dockerignore 24 | .github 25 | 26 | # Files that might appear in the root of a volume 27 | .DocumentRevisions-V100 28 | .fseventsd 29 | .Spotlight-V100 30 | .TemporaryItems 31 | .Trashes 32 | .VolumeIcon.icns 33 | .com.apple.timemachine.donotpresent 34 | 35 | # Directories potentially created on remote AFP share 36 | .AppleDB 37 | .AppleDesktop 38 | Network Trash Folder 39 | Temporary Items 40 | .apdisk 41 | 42 | # Byte-compiled / optimized / DLL files 43 | __pycache__/ 44 | *.py[cod] 45 | *$py.class 46 | 47 | # C extensions 48 | *.so 49 | 50 | # Distribution / packaging 51 | .Python 52 | build/ 53 | develop-eggs/ 54 | dist/ 55 | downloads/ 56 | eggs/ 57 | .eggs/ 58 | lib64/ 59 | parts/ 60 | sdist/ 61 | var/ 62 | wheels/ 63 | *.egg-info/ 64 | .installed.cfg 65 | *.egg 66 | 67 | # PyInstaller 68 | *.manifest 69 | *.spec 70 | 71 | # Installer logs 72 | pip-log.txt 73 | pip-delete-this-directory.txt 74 | 75 | # Unit test / coverage reports 76 | htmlcov/ 77 | .tox/ 78 | .coverage 79 | .coverage.* 80 | .cache 81 | nosetests.xml 82 | coverage.xml 83 | 84 | # Flask stuff: 85 | instance/ 86 | .webassets-cache 87 | 88 | # PyBuilder 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # Environments 98 | .env 99 | .venv 100 | env/ 101 | venv/ 102 | ENV/ 103 | 104 | # documentation 105 | docs 106 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: OctoBot-Script - Docker 2 | on: 3 | push: 4 | branches: 5 | - "master" 6 | tags: 7 | - "*" 8 | pull_request: 9 | 10 | jobs: 11 | lint: 12 | name: ubuntu-latest - Docker - lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Run hadolint 18 | uses: reviewdog/action-hadolint@v1 19 | with: 20 | github_token: ${{ secrets.github_token }} 21 | hadolint_ignore: DL3013 DL3008 22 | 23 | build: 24 | needs: lint 25 | name: ubuntu-latest - Docker - build & test & push 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | 31 | - name: Set Environment Variables 32 | run: | 33 | OWNER="$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]' | tr -d '-')" 34 | IMG=octobot-script 35 | echo "VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV 36 | echo "IMAGE=${OWNER}/${IMG}" >> $GITHUB_ENV 37 | echo "LATEST=latest" >> $GITHUB_ENV 38 | 39 | - name: Set up QEMU 40 | id: qemu-setup 41 | uses: docker/setup-qemu-action@master 42 | with: 43 | platforms: all 44 | 45 | - name: Print available platforms 46 | run: echo ${{ steps.qemu.outputs.platforms }} 47 | 48 | - name: Set up Docker Buildx 49 | id: buildx 50 | uses: docker/setup-buildx-action@master 51 | with: 52 | driver: docker-container 53 | use: true 54 | 55 | - name: Cache Docker layers 56 | uses: actions/cache@v4 57 | with: 58 | path: /tmp/.buildx-cache 59 | key: ${{ runner.os }}-buildx-${{ github.sha }} 60 | restore-keys: | 61 | ${{ runner.os }}-buildx- 62 | 63 | - name: Login to DockerHub 64 | if: github.event_name == 'push' 65 | uses: docker/login-action@v1 66 | with: 67 | username: ${{ secrets.DOCKERHUB_USERNAME }} 68 | password: ${{ secrets.DOCKERHUB_TOKEN }} 69 | 70 | - name: Build and push latest 71 | if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags') && github.ref == 'refs/heads/master' 72 | uses: docker/build-push-action@master 73 | with: 74 | context: . 75 | builder: ${{ steps.buildx.outputs.name }} 76 | platforms: linux/amd64,linux/arm64 77 | push: true 78 | tags: ${{ env.IMAGE }}:${{ env.LATEST }} 79 | build-args: | 80 | TENTACLES_URL_TAG=${{ env.LATEST }} 81 | cache-from: type=local,src=/tmp/.buildx-cache 82 | cache-to: type=local,dest=/tmp/.buildx-cache 83 | 84 | - name: Build and push on tag 85 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 86 | uses: docker/build-push-action@master 87 | with: 88 | context: . 89 | file: ./Dockerfile 90 | builder: ${{ steps.buildx.outputs.name }} 91 | platforms: linux/amd64,linux/arm64 92 | push: true 93 | tags: | 94 | ${{ env.IMAGE }}:${{ env.LATEST }} 95 | ${{ env.IMAGE }}:${{ env.VERSION }} 96 | cache-from: type=local,src=/tmp/.buildx-cache 97 | cache-to: type=local,dest=/tmp/.buildx-cache 98 | 99 | notify: 100 | if: ${{ failure() }} 101 | needs: 102 | - lint 103 | - build 104 | uses: Drakkar-Software/.github/.github/workflows/failure_notify_workflow.yml@master 105 | secrets: 106 | DISCORD_GITHUB_WEBHOOK: ${{ secrets.DISCORD_GITHUB_WEBHOOK }} 107 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: OctoBot-Script - CI 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | tags: 7 | - '*' 8 | pull_request: 9 | 10 | jobs: 11 | lint: 12 | uses: Drakkar-Software/.github/.github/workflows/python3_lint_workflow.yml@master 13 | with: 14 | project_main_package: octobot_script 15 | 16 | tests: 17 | name: ${{ matrix.os }} - Python - ${{ matrix.type }} - tests 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | os: [ macos-13, windows-latest, ubuntu-latest ] 22 | type: [ sources ] 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up Python 3.10 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: 3.10.x 30 | architecture: x64 31 | 32 | - name: Install dependencies 33 | run: pip install --prefer-binary -r dev_requirements.txt -r requirements.txt 34 | 35 | - name: Install tentacles from cli 36 | run: python start.py install_tentacles 37 | 38 | - name: Pytests 39 | run: pytest --cov=. --cov-config=.coveragerc --durations=0 -rw tests 40 | 41 | - name: Publish coverage 42 | if: matrix.type == 'sources' && github.event_name == 'push' 43 | run: coveralls 44 | continue-on-error: true 45 | env: 46 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 47 | 48 | publish: 49 | needs: 50 | - lint 51 | - tests 52 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 53 | name: Source distribution - Python - deploy 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v3 57 | - name: Set up Python 58 | uses: actions/setup-python@v4 59 | with: 60 | python-version: 3.10.x 61 | architecture: x64 62 | 63 | - name: Install dependencies 64 | run: pip install --prefer-binary -r dev_requirements.txt -r requirements.txt 65 | 66 | - name: Build sdist 67 | run: python setup.py sdist 68 | 69 | - name: Publish package 70 | run: | 71 | python -m twine upload --repository-url ${{ secrets.PYPI_OFFICIAL_UPLOAD_URL }} -u __token__ -p ${{ secrets.PYPI_TOKEN }} --skip-existing dist/* 72 | 73 | notify: 74 | if: ${{ failure() }} 75 | needs: 76 | - lint 77 | - tests 78 | - publish 79 | uses: Drakkar-Software/.github/.github/workflows/failure_notify_workflow.yml@master 80 | secrets: 81 | DISCORD_GITHUB_WEBHOOK: ${{ secrets.DISCORD_GITHUB_WEBHOOK }} 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.orig 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | .pytest_cache/ 13 | env/ 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 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | # *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # virtualenv 85 | .venv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | .spyproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # mkdocs documentation 97 | /site 98 | 99 | # mypy 100 | .mypy_cache/ 101 | 102 | # IDE 103 | .vscode/ 104 | .idea 105 | .gitpod.yml 106 | 107 | # Tentacles manager temporary files 108 | octobot/creator_temp/ 109 | creator_temp/ 110 | 111 | # Data 112 | backtesting/collector/data/ 113 | backtesting/data/ 114 | report.html 115 | 116 | # Tentacles 117 | tentacles 118 | downloaded_temp_tentacles 119 | 120 | # User config 121 | user/ 122 | temp_config.json 123 | 124 | *.csv 125 | *.ods 126 | *.c 127 | *.h 128 | 129 | # OctoBot logs 130 | logs 131 | 132 | # Debug 133 | cython_debug/ 134 | 135 | # dev env 136 | .env 137 | 138 | *.zip 139 | 140 | # tensorboard 141 | tensorboard_logs 142 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.0.25] - 2025-06-06 8 | ### Updated 9 | - OctoBot to 2.0.11 10 | 11 | ## [0.0.24] - 2025-05-25 12 | ### Updated 13 | - OctoBot to 2.0.10 14 | 15 | ## [0.0.23] - 2025-03-10 16 | ### Updated 17 | - OctoBot to 2.0.9 18 | 19 | ## [0.0.22] - 2025-01-29 20 | ### Updated 21 | - OctoBot to 2.0.8 22 | 23 | ## [0.0.21] - 2024-10-28 24 | ### Updated 25 | - OctoBot to 2.0.7 26 | ### Fixed 27 | - Indicators plotting 28 | 29 | ## [0.0.20] - 2024-10-11 30 | ### Updated 31 | - OctoBot to 2.0.6 32 | 33 | ## [0.0.19] - 2024-10-01 34 | ### Updated 35 | - OctoBot to 2.0.5 36 | 37 | ## [0.0.18] - 2024-07-10 38 | ### Updated 39 | - OctoBot to 2.0.1 40 | 41 | ## [0.0.17] - 2024-05-01 42 | ### Updated 43 | - OctoBot to 1.0.10 44 | 45 | ## [0.0.16] - 2023-01-14 46 | ### Updated 47 | - OctoBot to 1.0.6 48 | 49 | ## [0.0.15] - 2023-12-15 50 | ### Updated 51 | - OctoBot to 1.0.4 52 | 53 | ## [0.0.14] - 2023-10-30 54 | ### Updated 55 | - OctoBot to 1.0.2 56 | 57 | ## [0.0.13] - 2023-10-09 58 | ### Updated 59 | - Renamed from OctoBot Pro to OctoBot Script 60 | 61 | ## [0.0.12] - 2023-09-27 62 | ### Added 63 | - Dockerfile 64 | - Docker image on dockerhub 65 | ### Update 66 | - Update to OctoBot 1.0.0 67 | 68 | ## [0.0.11] - 2023-09-23 69 | ### Update 70 | - Update to OctoBot 0.4.54 71 | 72 | ## [0.0.10] - 2022-05-13 73 | ### Update 74 | - Added python 3.9 and 3.10 support 75 | - Update to OctoBot 0.4.50 76 | 77 | ## [0.0.9] - 2022-05-02 78 | ### Update 79 | - Update to OctoBot 0.4.49 80 | 81 | ## [0.0.8] - 2022-03-24 82 | ### Update 83 | - Update to OctoBot 0.4.45 84 | 85 | ## [0.0.7] - 2022-03-07 86 | ### Update 87 | - Update to OctoBot 0.4.41 88 | 89 | ## [0.0.6] - 2022-01-21 90 | ### Fix 91 | - Typeerror and report issues 92 | 93 | ## [0.0.5] - 2022-01-14 94 | ### Fix 95 | - Installation: remove cryptofeed requirement in OctoBot 96 | 97 | ## [0.0.4] - 2022-30-12 98 | ### Added 99 | - Report generation time 100 | 101 | ## [0.0.3] - 2022-29-12 102 | ### Fixed 103 | - Install 104 | 105 | ## [0.0.2] - 2022-29-12 106 | ### Updated 107 | - Install method 108 | 109 | ## [0.0.1] - 2022-10-12 110 | ### Added 111 | - OctoBot Pro alpha version 112 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster AS base 2 | 3 | WORKDIR /app 4 | 5 | # requires git to install requirements with git+https 6 | RUN apt-get update \ 7 | && apt-get install -y --no-install-recommends build-essential git gcc binutils 8 | 9 | COPY . . 10 | 11 | RUN pip3 install --no-cache-dir -U setuptools wheel pip \ 12 | && pip3 install --no-cache-dir -r requirements.txt \ 13 | && python3 setup.py install 14 | 15 | ENTRYPOINT ["bash"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include octobot_script/config *.json *.ini 2 | recursive-include octobot_script/resources *.js *.css *.html 3 | 4 | include README.md 5 | include LICENSE 6 | include CHANGELOG.md 7 | include requirements.txt 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OctoBot-Script [0.0.25](https://github.com/Drakkar-Software/OctoBot-Script/tree/master/CHANGELOG.md) 2 | [![PyPI](https://img.shields.io/pypi/v/OctoBot-Script.svg?logo=pypi)](https://pypi.python.org/pypi/octobot-script/) 3 | [![Downloads](https://static.pepy.tech/badge/OctoBot-Script/month)](https://pepy.tech/project/octobot-script) 4 | [![Dockerhub](https://img.shields.io/docker/pulls/drakkarsoftware/OctoBot-Script.svg?logo=docker)](https://hub.docker.com/r/drakkarsoftware/octobot-script) 5 | [![Github-Action-CI](https://github.com/Drakkar-Software/OctoBot-Script/actions/workflows/main.yml/badge.svg)](https://github.com/Drakkar-Software/OctoBot-Script/actions/workflows/main.yml) 6 | 7 | ## OctoBot-Script Community 8 | [![Telegram Chat](https://img.shields.io/badge/telegram-chat-green.svg?logo=telegram&label=Telegram)](https://t.me/+366CLLZ2NC0xMjFk) 9 | [![Discord](https://img.shields.io/discord/530629985661222912.svg?logo=discord&label=Discord)](https://discord.com/invite/vHkcb8W) 10 | [![Twitter](https://img.shields.io/twitter/follow/DrakkarsOctobot.svg?label=twitter&style=social)](https://twitter.com/DrakkarsOctoBot) 11 | 12 | 13 | ## OctoBot Script is the Quant framework by OctoBot 14 | 15 | > OctoBot Script is in alpha version 16 | 17 | Documentation available at [octobot.cloud/en/guides/octobot-script](https://www.octobot.cloud/en/guides/octobot-script?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=octobot_script_readme) 18 | 19 | 20 | ## Install OctoBot Script from pip 21 | 22 | > OctoBot Script requires **Python 3.10** 23 | 24 | ``` {.sourceCode .bash} 25 | python3 -m pip install OctoBot wheel appdirs==1.4.4 26 | python3 -m pip install octobot-script 27 | ``` 28 | 29 | ## Example of a backtesting script 30 | 31 | ### Script 32 | ``` python 33 | import asyncio 34 | import tulipy # Can be any TA library. 35 | import octobot_script as obs 36 | 37 | 38 | async def rsi_test(): 39 | async def strategy(ctx): 40 | # Will be called at each candle. 41 | if run_data["entries"] is None: 42 | # Compute entries only once per backtest. 43 | closes = await obs.Close(ctx, max_history=True) 44 | times = await obs.Time(ctx, max_history=True, use_close_time=True) 45 | rsi_v = tulipy.rsi(closes, period=ctx.tentacle.trading_config["period"]) 46 | delta = len(closes) - len(rsi_v) 47 | # Populate entries with timestamps of candles where RSI is 48 | # bellow the "rsi_value_buy_threshold" configuration. 49 | run_data["entries"] = { 50 | times[index + delta] 51 | for index, rsi_val in enumerate(rsi_v) 52 | if rsi_val < ctx.tentacle.trading_config["rsi_value_buy_threshold"] 53 | } 54 | await obs.plot_indicator(ctx, "RSI", times[delta:], rsi_v, run_data["entries"]) 55 | if obs.current_live_time(ctx) in run_data["entries"]: 56 | # Uses pre-computed entries times to enter positions when relevant. 57 | # Also, instantly set take profits and stop losses. 58 | # Position exists could also be set separately. 59 | await obs.market(ctx, "buy", amount="10%", stop_loss_offset="-15%", take_profit_offset="25%") 60 | 61 | # Configuration that will be passed to each run. 62 | # It will be accessible under "ctx.tentacle.trading_config". 63 | config = { 64 | "period": 10, 65 | "rsi_value_buy_threshold": 28, 66 | } 67 | 68 | # Read and cache candle data to make subsequent backtesting runs faster. 69 | data = await obs.get_data("BTC/USDT", "1d", start_timestamp=1505606400) 70 | run_data = { 71 | "entries": None, 72 | } 73 | # Run a backtest using the above data, strategy and configuration. 74 | res = await obs.run(data, strategy, config) 75 | print(res.describe()) 76 | # Generate and open report including indicators plots 77 | await res.plot(show=True) 78 | # Stop data to release local databases. 79 | await data.stop() 80 | 81 | 82 | # Call the execution of the script inside "asyncio.run" as 83 | # OctoBot-Script runs using the python asyncio framework. 84 | asyncio.run(rsi_test()) 85 | ``` 86 | 87 | ### Generated report 88 | ![report-p1](https://raw.githubusercontent.com/Drakkar-Software/OctoBot-Script/assets/images/report_1.jpg) 89 | 90 | ### Join the community 91 | We recently created a telegram channel dedicated to OctoBot Script. 92 | 93 | [![Telegram News](https://img.shields.io/static/v1?label=Telegram%20chat&message=Join&logo=telegram&&color=007bff&style=for-the-badge)](https://t.me/+366CLLZ2NC0xMjFk) 94 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Getting Started](README.md) 4 | * [OctoBot](https://www.octobot.cloud) 5 | 6 | ## Trading strategies 7 | 8 | * [Strategy basis](docs/strategies/basis.md) 9 | 10 | ## Trading keywords 11 | 12 | * [Creating orders](docs/trading/orders.md) 13 | 14 | ## Plotting 15 | 16 | * [Plotting](docs/plotting/basis.md) 17 | 18 | ## Trading data 19 | 20 | * [Fetching data](docs/data/fetching_data.md) 21 | -------------------------------------------------------------------------------- /ai-example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import tulipy 3 | import gymnasium as gym 4 | from datetime import datetime, timedelta 5 | import numpy as np 6 | import time 7 | from tensorflow.keras.callbacks import TensorBoard 8 | import argparse 9 | 10 | import octobot_commons.symbols as symbols 11 | import octobot_script as obs 12 | 13 | async def basic_evaluation_function(ctx): 14 | closes = await obs.Close(ctx, max_history=True) 15 | open = await obs.Open(ctx, limit=30) 16 | high = await obs.High(ctx, limit=30) 17 | low = await obs.Low(ctx, limit=30) 18 | vol = await obs.Volume(ctx, limit=30) 19 | rsi_v = tulipy.rsi(closes, period=10) 20 | ema_values = tulipy.ema(closes, period=21) 21 | 22 | try: 23 | if (len(rsi_v) > 15 and len(ema_values) > 15): 24 | return np.array([ 25 | closes[-10:], 26 | open[-10:], 27 | high[-10:], 28 | low[-10:], 29 | vol[-10:], 30 | rsi_v[-15:], 31 | ema_values[-15:] 32 | ], dtype=np.float32).flatten() 33 | else: 34 | return np.zeros(80, dtype=np.float32) 35 | except ValueError: 36 | return np.zeros(80, dtype=np.float32) 37 | 38 | async def run_strategy(data, env, agent, symbol, time_frame, is_training=False, plot=False): 39 | async def strategy(ctx): 40 | state = None 41 | if not env.env.get_wrapper_attr('is_reset'): 42 | state = await env.reset(options={ 43 | 'ctx': ctx 44 | }) 45 | else: 46 | state = await env.get_obs(ctx) 47 | 48 | action = agent.act(state) 49 | next_state, reward, done, info = await env.step({ 50 | 'ctx': ctx, 51 | 'content': action 52 | }) 53 | 54 | if is_training: 55 | agent.remember(state, action, reward, next_state, done) 56 | 57 | # Run a backtest using the above data, strategy and configuration. 58 | res = await obs.run(data, strategy, {}, enable_logs=False) 59 | 60 | if plot: 61 | print(res.describe()) 62 | await res.plot(show=True) 63 | 64 | def init_argparse() -> argparse.ArgumentParser: 65 | parser = argparse.ArgumentParser() 66 | parser.add_argument("-ex", "--exchange", type=str, default="binance") 67 | parser.add_argument("-s", "--symbol", type=str, default="BTC/USDT") 68 | parser.add_argument("-tf", "--timeframe", type=str, default="1d") 69 | parser.add_argument("-e", "--episode", type=int, default=1) 70 | parser.add_argument('-b', '--batch_size', type=int, default=32, 71 | help='batch size for experience replay') 72 | parser.add_argument("-t", "--train", action=argparse.BooleanOptionalAction) 73 | parser.add_argument("-p", "--plot", action=argparse.BooleanOptionalAction) 74 | parser.add_argument('-w', '--weights', type=str, help='a trained model weights') 75 | parser.add_argument("-d", "--days", type=int, default=365) 76 | parser.add_argument("-ev", "--evaluate", action=argparse.BooleanOptionalAction) 77 | parser.add_argument("-ep", "--epochs", type=int, default=20) 78 | return parser 79 | 80 | 81 | 82 | def main(): 83 | parser = init_argparse() 84 | args = parser.parse_args() 85 | 86 | timestamp = time.strftime('%Y%m%d%H%M') 87 | symbol = symbols.parse_symbol(args.symbol) 88 | time_frame = args.timeframe 89 | data = asyncio.run(obs.get_data(symbol.merged_str_symbol(), time_frame, exchange=args.exchange, start_timestamp=int(float(str((datetime.now() - timedelta(days=args.days)).timestamp()))))) # start_timestamp=1505606400 90 | 91 | action_size = 9 92 | gym_env = gym.make(action_size=action_size, id='TradingEnv', name= "test", dynamic_feature_functions=[basic_evaluation_function], traded_symbols=[symbol]) 93 | agent = obs.DQNAgent(action_size) 94 | 95 | logdir = "tensorboard_logs/scalars/" + datetime.now().strftime("%Y%m%d-%H%M%S") 96 | tensorboard_callback = TensorBoard(log_dir=logdir, histogram_freq=1, write_images=False, batch_size=args.batch_size) 97 | 98 | if args.weights: 99 | print(f"Loading model {args.weights}...") 100 | 101 | # load trained weights 102 | agent.load(args.weights) 103 | elif not args.train: 104 | print("weights has not be provided when using model!") 105 | return 106 | 107 | for episode in range(args.episode): 108 | print(f"Starting episode {episode}...") 109 | asyncio.run(run_strategy(data, gym_env, agent, symbol, time_frame, is_training=args.train, plot=args.plot)) 110 | 111 | if args.train and len(agent.memory) > args.batch_size: 112 | print("Starting replay...") 113 | score = agent.replay(args.batch_size, args.epochs, args.evaluate, tensorboard_callback) 114 | if args.evaluate: 115 | print(f"Score = {score}") 116 | 117 | if args.train and (episode + 1) % 10 == 0: # checkpoint weights 118 | print("Saving...") 119 | agent.save(f"weights/{timestamp}-dqn.h5") 120 | 121 | if args.train: 122 | agent.save(f"weights/{timestamp}-final-dqn.h5") 123 | 124 | asyncio.run(data.stop()) 125 | 126 | main() 127 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=7.1 2 | pytest-asyncio>=0.19 3 | pytest-pep8 4 | pytest-cov 5 | pytest-xdist 6 | 7 | mock>=4.0.2 8 | 9 | 10 | coverage 11 | coveralls 12 | 13 | twine 14 | pip 15 | setuptools 16 | wheel 17 | 18 | pur 19 | 20 | pylint 21 | -------------------------------------------------------------------------------- /docs/data/fetching_data.md: -------------------------------------------------------------------------------- 1 | # Fetching trading data 2 | 3 | In order to run a backtest, OctoBot-Script requires historical 4 | trading data, which is at least candles history. 5 | 6 | ## Fetching new data 7 | When using OctoBot-Script, historical data can be fetched using: 8 | `await op.get_data(symbol, time frame)` 9 | 10 | Where: 11 | - symbol: the trading symbol to fetch data from. It can also be a list of symbols 12 | - time frame: the time frame to fetch (1h, 4h, 1d, etc). It can also be a list of time frames 13 | 14 | Optional arguments: 15 | - start_timestamp: the unix timestamp to start fetching data from. Use [this converter](https://www.epochconverter.com/) if you are unsure what you should use. 16 | - exchange: the exchange to fetch data from. Default is "binance" 17 | - exchange_type: the exchange trading type to fetch data from. Default is "spot", "future" is also possible on supported exchanges 18 | ``` python 19 | data = await op.get_data("BTC/USDT", "1d", start_timestamp=1505606400) 20 | ``` 21 | 22 | ## Re-using fetched data 23 | Calling `data = await op.get_data` will save the downloaded data into the `backtesting/data` local folder. 24 | If you want to speedup subsequent calls, you can provide the `data_file` optional argument to read 25 | data from this file instead of downloading historical data. This also makes it possible to run a 26 | script while being offline. 27 | 28 | You can get the name of the downloaded backtesting file by accessing 29 | `data.data_files[0]` 30 | 31 | ``` python 32 | data = await op.get_data("BTC/USDT", "1d", start_timestamp=1505606400) 33 | # print the name of the downloaded data file 34 | print(data.data_files[0]) 35 | ``` 36 | 37 | ``` python 38 | datafile = "ExchangeHistoryDataCollector_1671754854.5234916.data" 39 | # will not download historical data as a local data_file is provided 40 | data = await op.get_data("BTC/USDT", "1d", start_timestamp=1505606400, data_file=datafile) 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/plotting/basis.md: -------------------------------------------------------------------------------- 1 | # Plotting 2 | 3 | ## Plotting indicators 4 | Indicators and associated signals can be easily plotted using the 5 | `plot_indicator(ctx, name, x, y, signals)` keyword. 6 | 7 | Where: 8 | - `name`: name of the indicator on the chart 9 | - `x`: values to use for the x axis 10 | - `y`: values to use for the y axis 11 | - `signal`: (optional) x values for which a signal is fired 12 | 13 | Example where the goal is to plot the value of the rsi indicator from 14 | the [example script](../../#script). 15 | ``` python 16 | await op.plot_indicator(ctx, "RSI", time_values, indicator_values, signal_times) 17 | ``` 18 | 19 | ## Plotting anything 20 | 21 | Anything can be plotted using the `plot(ctx, name, ...)` keyword. 22 | The plot arguments are converted into [plotly](https://plotly.com/javascript/) charts parameters. 23 | 24 | Where: 25 | - `name`: name of the indicator on the chart 26 | 27 | Optional arguments: 28 | - `x`: values to use for the x axis 29 | - `y`: values to use for the y axis 30 | - `z`: values to use for the z axis 31 | - `text`: point labels 32 | - `mode`: plotly mode ("lines", "markers", "lines+markers", "lines+markers+text", "none") 33 | - `chart`: "main-chart" or "sub-chart" (default is "sub-chart") 34 | - `own_yaxis`: when True, uses an independent y axis for this plot (default is False) 35 | - `color`: color the of plot 36 | - `open`: open values for a candlestick chart 37 | - `high`: high values for a candlestick chart 38 | - `low`: low values for a candlestick chart 39 | - `close`: close values for a candlestick chart 40 | - `volume`: volume values for a candlestick chart 41 | - `low`: low values for a candlestick chart 42 | 43 | Example: 44 | ``` python 45 | await op.plot(ctx, "RSI", x=time_values, y=indicator_values, mode="markers") 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/strategies/basis.md: -------------------------------------------------------------------------------- 1 | # OctoBot-Script strategies 2 | 3 | On OctoBot-Script, a trading strategy is a python async function that will be called at new price data. 4 | ``` python 5 | async def strategy(ctx): 6 | # your strategy content 7 | ``` 8 | 9 | In most cases, a strategy will: 10 | 1. Read price data 11 | 2. Use technical evaluators or statistics 12 | 3. Decide to take (or not take) action depending on its configuration 13 | 4. Create / cancel or edit orders (see [Creating orders](../../docs/trading/orders.md)) 14 | 15 | As OctoBot-Script strategies are meant for backtesting, it is possible to create a strategy in 2 ways: 16 | ## Pre-computed strategies 17 | Pre-computed are only possible in backtesting: since the data is already known, when dealing with technical 18 | evaluator based strategies, it is possible to compute the values of the evaluators for the whole backtest at once. 19 | This approach is faster than iterative strategies as evaluators call only called once. 20 | 21 | Warning: when writing a pre-computed strategy, always make sure to associate the evaluator values to the 22 | right time otherwise you might be reading data from the past of the future when running the strategy. 23 | 24 | ``` python 25 | config = { 26 | "period": 10, 27 | "rsi_value_buy_threshold": 28, 28 | } 29 | run_data = { 30 | "entries": None, 31 | } 32 | async def strategy(ctx): 33 | if run_data["entries"] is None: 34 | # 1. Read price data 35 | closes = await op.Close(ctx, max_history=True) 36 | times = await op.Time(ctx, max_history=True, use_close_time=True) 37 | # 2. Use technical evaluators or statistics 38 | rsi_v = tulipy.rsi(closes, period=ctx.tentacle.trading_config["period"]) 39 | delta = len(closes) - len(rsi_v) 40 | # 3. Decide to take (or not take) action depending on its configuration 41 | run_data["entries"] = { 42 | times[index + delta] 43 | for index, rsi_val in enumerate(rsi_v) 44 | if rsi_val < ctx.tentacle.trading_config["rsi_value_buy_threshold"] 45 | } 46 | await op.plot_indicator(ctx, "RSI", times[delta:], rsi_v, run_data["entries"]) 47 | if op.current_live_time(ctx) in run_data["entries"]: 48 | # 4. Create / cancel or edit orders 49 | await op.market(ctx, "buy", amount="10%", stop_loss_offset="-15%", take_profit_offset="25%") 50 | ``` 51 | This pre-computed strategy computes entries using the RSI: times of favorable entries are stored into 52 | `run_data["entries"]` which is defined outside on the `strategy` function in order to keep its values 53 | throughout iterations. 54 | 55 | Please note the `max_history=True` in `op.Close` and `op.Time` keywords. This is allowing to select 56 | data using the whole run available data and only call `tulipy.rsi` once and populate `run_data["entries"]` 57 | only once. 58 | 59 | In each subsequent call, `run_data["entries"] is None` will be `True` and only the last 2 lines of 60 | the strategy will be executed. 61 | 62 | ## Iterative strategies 63 | ``` python 64 | config = { 65 | "period": 10, 66 | "rsi_value_buy_threshold": 28, 67 | } 68 | async def strategy(ctx): 69 | # 1. Read price data 70 | close = await op.Close(ctx) 71 | if len(close) <= ctx.tentacle.trading_config["period"]: 72 | # not enough data to compute RSI 73 | return 74 | # 2. Use technical evaluators or statistics 75 | rsi_v = tulipy.rsi(close, period=ctx.tentacle.trading_config["period"]) 76 | # 3. Decide to take (or not take) action depending on its configuration 77 | if rsi_v[-1] < ctx.tentacle.trading_config["rsi_value_buy_threshold"]: 78 | # 4. Create / cancel or edit orders 79 | await op.market(ctx, "buy", amount="10%", stop_loss_offset="-15%", take_profit_offset="25%") 80 | ``` 81 | This iterative strategy is similar to the above pre-computed strategy except that it is evaluating the RSI 82 | at each candle to know if an entry should be created. 83 | 84 | This type of strategy is simpler to create than a pre-computed strategy and can be used in 85 | OctoBot live trading. 86 | 87 | ## Running a strategy 88 | 89 | When running a backtest, a strategy should be referenced alongside: 90 | - The [data it should be run on](../../docs/data/fetching_data.md) using `op.run`: 91 | - Its configuration (a dict in above examples, it could be anything) 92 | 93 | ``` python 94 | res = await op.run(data, strategy, config) 95 | ``` 96 | 97 | Have a look [here](../../#script) for a full example of 98 | how to run a strategy within a python script. 99 | -------------------------------------------------------------------------------- /docs/trading/orders.md: -------------------------------------------------------------------------------- 1 | # Orders 2 | 3 | Orders can be created using the following keywords: 4 | - `market` 5 | - `limit` 6 | - `stop_loss` 7 | - `trailing_market` 8 | 9 | ## Amount 10 | Each order accept the following optional arguments: 11 | - `amount`: for spot and futures trading 12 | - `target_position`: futures trading only 13 | 14 | To specify the amount per order, use the following syntax: 15 | - `0.1` to trade 0.1 BTC on BTC/USD 16 | - `2%` to trade 2% of the total portfolio value 17 | - `12%a` to trade 12% of the available holdings 18 | 19 | ``` python 20 | # create a buy market order using 10% of the total portfolio 21 | await op.market(ctx, "buy", amount="10%") 22 | ``` 23 | 24 | ## Price 25 | Orders set their price using the `offset` argument. 26 | 27 | To specify the order price, use the following syntax: 28 | - `10` to set the price 10 USD above the current BTC/USD market price 29 | - `2%` to set the price 2% USD above the current BTC/USD market price 30 | - `@15555` to set the price at exactly 15555 USD regardless of the current BTC/USD market price 31 | 32 | ``` python 33 | # create a buy limit order of 0.2 units (BTC when trading BTC/USD) 34 | # with a price at 1% bellow the current price 35 | await op.limit(ctx, "buy", amount="0.2", offset="-1%") 36 | ``` 37 | 38 | Note: market orders do not accept the `offset` argument. 39 | 40 | ## Automated take profit and stop losses 41 | When creating orders, it is possible to automate the associated 42 | stop loss and / or take profits. When doing to, the associated take profit/stop loss will have 43 | the same amount as the initial order. 44 | 45 | Their price can be set according to the same rules as the initial order price 46 | (the `offset` argument) using the following optional argument: 47 | - `stop_loss_offset`: automate a stop loss creation when the initial order is filled and set the stop loss price 48 | - `take_profit_offset`: automate a take profit creation when the initial order is filled and set the take profit price 49 | 50 | ``` python 51 | # create a buy limit order of 0.2 units (BTC when trading BTC/USD) with: 52 | # - price at 1% bellow the current price 53 | # - stop loss at 10% loss 54 | # - take profit at 15% profit 55 | await op.limit(ctx, "buy", amount="0.2", offset="-1%", stop_loss_offset="-10%", take_profit_offset="15%") 56 | ``` 57 | 58 | {% hint style="info" %} 59 | When using both `stop_loss_offset` and `take_profit_offset`, two orders will be created after the initial order fill. 60 | Those two orders will be grouped together, meaning that if one is cancelled or filled, the other will be cancelled. 61 | {% endhint %} 62 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import tulipy # Can be any TA library. 3 | import octobot_script as obs 4 | 5 | 6 | async def rsi_test(): 7 | async def strategy(ctx): 8 | # Will be called at each candle. 9 | if run_data["entries"] is None: 10 | # Compute entries only once per backtest. 11 | closes = await obs.Close(ctx, max_history=True) 12 | times = await obs.Time(ctx, max_history=True, use_close_time=True) 13 | rsi_v = tulipy.rsi(closes, period=ctx.tentacle.trading_config["period"]) 14 | delta = len(closes) - len(rsi_v) 15 | # Populate entries with timestamps of candles where RSI is 16 | # bellow the "rsi_value_buy_threshold" configuration. 17 | run_data["entries"] = { 18 | times[index + delta] 19 | for index, rsi_val in enumerate(rsi_v) 20 | if rsi_val < ctx.tentacle.trading_config["rsi_value_buy_threshold"] 21 | } 22 | await obs.plot_indicator(ctx, "RSI", times[delta:], rsi_v, run_data["entries"]) 23 | if obs.current_live_time(ctx) in run_data["entries"]: 24 | # Uses pre-computed entries times to enter positions when relevant. 25 | # Also, instantly set take profits and stop losses. 26 | # Position exists could also be set separately. 27 | await obs.market(ctx, "buy", amount="10%", stop_loss_offset="-15%", take_profit_offset="25%") 28 | 29 | # Configuration that will be passed to each run. 30 | # It will be accessible under "ctx.tentacle.trading_config". 31 | config = { 32 | "period": 10, 33 | "rsi_value_buy_threshold": 28, 34 | } 35 | 36 | # Read and cache candle data to make subsequent backtesting runs faster. 37 | data = await obs.get_data("BTC/USDT", "1d", start_timestamp=1505606400) 38 | run_data = { 39 | "entries": None, 40 | } 41 | # Run a backtest using the above data, strategy and configuration. 42 | res = await obs.run(data, strategy, config) 43 | print(res.describe()) 44 | # Generate and open report including indicators plots 45 | await res.plot(show=True) 46 | # Stop data to release local databases. 47 | await data.stop() 48 | 49 | 50 | # Call the execution of the script inside "asyncio.run" as 51 | # OctoBot-Script runs using the python asyncio framework. 52 | asyncio.run(rsi_test()) 53 | -------------------------------------------------------------------------------- /octobot_script/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | PROJECT_NAME = "OctoBot-Script" 18 | AUTHOR = "Drakkar-Software" 19 | VERSION = "0.0.25" # major.minor.revision => don't forget to also update the setup.py version 20 | 21 | 22 | def _use_module_local_tentacles(): 23 | import sys 24 | import os 25 | import appdirs 26 | if os.getenv("USE_CUSTOM_TENTACLES", "").lower() == "true": 27 | # do not use octobot_script/imports tentacles 28 | # WARNING: in this case, all the required tentacles imports still have to work 29 | # and therefore be bound to another tentacles folder 30 | return 31 | # import tentacles from user-appdirs/imports directory 32 | dirs = appdirs.AppDirs(PROJECT_NAME, AUTHOR, VERSION) 33 | internal_import_path = os.path.join(dirs.user_data_dir, "imports") 34 | sys.path.insert(0, internal_import_path) 35 | 36 | 37 | # run this before any other code as only octobot_script module-local tentacles should be used 38 | _use_module_local_tentacles() 39 | 40 | try: 41 | # import tentacles from octobot_script/imports directory after "_use_local_tentacles()" call 42 | from tentacles.Meta.Keywords import * 43 | # populate tentacles config helpers 44 | import octobot_tentacles_manager.loaders as loaders 45 | import octobot_script.internal.octobot_mocks as octobot_mocks 46 | loaders.reload_tentacle_by_tentacle_class( 47 | tentacles_path=octobot_mocks.get_imported_tentacles_path() 48 | ) 49 | # do not expose those when importing this file 50 | loaders = octobot_mocks = None 51 | 52 | except ImportError: 53 | # tentacles not available during first install 54 | pass 55 | 56 | from octobot_script.constants import * 57 | from octobot_script.api import * 58 | from octobot_script.model import * 59 | from octobot_script.ai import * 60 | -------------------------------------------------------------------------------- /octobot_script/ai/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from octobot_script.ai.environments import * 3 | from octobot_script.ai.models import * 4 | from octobot_script.ai.agents import * 5 | 6 | from gymnasium.envs.registration import register 7 | 8 | register( 9 | id='TradingEnv', 10 | entry_point='octobot_script.ai.environments:TradingEnv', 11 | disable_env_checker = True 12 | ) 13 | except ImportError: 14 | pass 15 | -------------------------------------------------------------------------------- /octobot_script/ai/agents.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | import random 3 | import numpy as np 4 | 5 | from octobot_script.ai.models import mlp 6 | 7 | class DQNAgent: 8 | def __init__(self, action_size): 9 | self.action_size = action_size 10 | self.memory = deque(maxlen=2000) 11 | self.gamma = 0.95 # discount rate 12 | self.epsilon = 1.0 # exploration rate 13 | self.epsilon_min = 0.01 14 | self.epsilon_decay = 0.995 15 | self.model = mlp(action_size) 16 | 17 | def remember(self, state, action, reward, next_state, done): 18 | self.memory.append((state, action, reward, next_state, done)) 19 | 20 | def act(self, state): 21 | if np.random.rand() <= self.epsilon: 22 | return random.randrange(self.action_size) 23 | act_values = self.model.predict(state) 24 | return np.argmax(act_values[0]) # returns action 25 | 26 | def replay(self, batch_size=32, epochs=1, evaluate=False, tensorboard_callback=None): 27 | # pylint: disable=unsubscriptable-object 28 | """ vectorized implementation; 30x speed up compared with for loop """ 29 | minibatch = random.sample(self.memory, batch_size) 30 | 31 | states = np.array([tup[0][0] for tup in minibatch]) 32 | actions = np.array([tup[1] for tup in minibatch]) 33 | rewards = np.array([tup[2] for tup in minibatch]) 34 | next_states = np.array([tup[3][0] for tup in minibatch]) 35 | done = np.array([tup[4] for tup in minibatch]) 36 | 37 | # Q(s', a) 38 | target = rewards + self.gamma * np.amax(self.model.predict(next_states), axis=1) 39 | # end state target is reward itself (no lookahead) 40 | target[done] = rewards[done] 41 | 42 | # Q(s, a) 43 | target_f = self.model.predict(states) 44 | # make the agent to approximately map the current state to future discounted reward 45 | target_f[range(batch_size), actions] = target 46 | 47 | self.model.fit(states, target_f, batch_size=batch_size, epochs=epochs, verbose=0, callbacks=[tensorboard_callback]) 48 | 49 | if self.epsilon > self.epsilon_min: 50 | self.epsilon *= self.epsilon_decay 51 | 52 | if evaluate: 53 | return self.model.evaluate(states, target_f, batch_size=32) 54 | return 0 55 | 56 | def load(self, name): 57 | self.model.load_weights(name) 58 | 59 | def save(self, name): 60 | self.model.save_weights(name) -------------------------------------------------------------------------------- /octobot_script/ai/environments.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=maybe-no-member 2 | import gymnasium as gym 3 | from gymnasium import spaces 4 | import numpy as np 5 | 6 | import octobot_script as obs 7 | import octobot_trading.errors as octobot_trading_errors 8 | import octobot_trading.api as trading_api 9 | 10 | def basic_reward_function(current_portfolio_value, previous_portfolio_value, current_profitability, market_profitability, created_orders): 11 | if previous_portfolio_value is None: 12 | return 0 13 | try: 14 | pf_reward = np.log(float(current_portfolio_value) / float(previous_portfolio_value)) 15 | prof_reward = np.log(float(current_profitability) / float(market_profitability)) 16 | reward = 0 if np.isnan(pf_reward) else pf_reward + 0 if np.isnan(prof_reward) else prof_reward 17 | return reward 18 | except ZeroDivisionError: 19 | return 0 20 | 21 | async def basic_trade_function(ctx, action): 22 | try: 23 | created_orders = [] 24 | if action == 0: 25 | # TODO cancel orders 26 | pass 27 | elif action == 1: 28 | created_orders.append(await obs.market( 29 | ctx, 30 | "buy", 31 | amount=f"10%" 32 | )) 33 | elif action == 2: 34 | created_orders.append(await obs.market( 35 | ctx, 36 | "sell", 37 | amount=f"{10}%" 38 | )) 39 | elif action in [3, 4, 5]: 40 | created_orders.append(await obs.limit( 41 | ctx, 42 | "buy", 43 | amount=f"{1 if action == 3 else 10 if action == 4 else 30}%", 44 | offset=f"-{1 if action == 3 else 2 if action == 4 else 3}%", 45 | )) 46 | elif action in [6, 7, 8]: 47 | created_orders.append(await obs.limit( 48 | ctx, 49 | "sell", 50 | amount=f"{1 if action == 6 else 10 if action == 7 else 30}%", 51 | offset=f"{1 if action == 6 else 2 if action == 7 else 3}%", 52 | )) 53 | else: 54 | # Nothing for now 55 | pass 56 | return created_orders 57 | except TypeError: 58 | pass 59 | 60 | # TODO move somewhere else 61 | def get_profitabilities(ctx): 62 | return trading_api.get_profitability_stats(ctx.exchange_manager) 63 | 64 | # TODO move somewhere else 65 | def get_open_orders(ctx): 66 | return [] # TODO 67 | 68 | # TODO move somewhere else 69 | def get_current_portfolio_value(ctx): 70 | return trading_api.get_current_portfolio_value(ctx.exchange_manager) 71 | 72 | # TODO move somewhere else 73 | def get_current_portfolio(ctx): 74 | return trading_api.portfolio.get_portfolio(ctx.exchange_manager) 75 | 76 | def get_flatten_pf(current_portfolio, symbol): 77 | return np.array([float(current_portfolio[symbol.base].available), 78 | float(current_portfolio[symbol.base].total), 79 | float(current_portfolio[symbol.quote].available), 80 | float(current_portfolio[symbol.quote].total)], dtype=np.float32) 81 | 82 | class TradingEnv(gym.Env): 83 | def __init__(self, 84 | action_size=1, 85 | dynamic_feature_functions = [], 86 | reward_function = basic_reward_function, 87 | trade_function = basic_trade_function, 88 | max_episode_duration = 'max', 89 | verbose = 1, 90 | name = "Rl", 91 | traded_symbols=[] 92 | ): 93 | self.max_episode_duration = max_episode_duration 94 | self.name = name 95 | self.verbose = verbose 96 | self.is_reset = False 97 | 98 | self.traded_symbols = traded_symbols 99 | self.static_features = [] # TODO there are computed once before being used in the environement 100 | self.dynamic_feature_functions = dynamic_feature_functions # are computed at each step of the environment 101 | self._nb_features = 79 + len(self.traded_symbols) * 4 + len(self.static_features) + len(self.dynamic_feature_functions) 102 | 103 | self.reward_function = reward_function 104 | self.trade_function = trade_function 105 | self.max_episode_duration = max_episode_duration 106 | 107 | self.action_space = spaces.Discrete(action_size) 108 | self.observation_space = spaces.Box( 109 | -np.inf, 110 | np.inf, 111 | shape = [self._nb_features] 112 | ) 113 | 114 | self.log_metrics = [] 115 | self._previous_portfolio_value = None 116 | 117 | async def get_obs(self, ctx): 118 | flatten_pf = np.concatenate([get_flatten_pf(get_current_portfolio(ctx), symbol) for symbol in self.traded_symbols]) 119 | # TODO open orders 120 | dynamic_obs = [] 121 | for dynamic_feature_function in self.dynamic_feature_functions: 122 | dynamic_obs.append(await dynamic_feature_function(ctx)) 123 | return np.concatenate([dynamic_obs[0], flatten_pf]) 124 | 125 | async def reset(self, seed = None, options = None): 126 | super().reset(seed = seed) 127 | self.is_reset = True 128 | self._step = 0 129 | self._idx = 0 130 | if self.max_episode_duration != 'max': 131 | self._idx = np.random.randint( 132 | low = self._idx, 133 | high = len(self.df) - self.max_episode_duration - self._idx 134 | ) 135 | 136 | return await self.get_obs(options['ctx']) 137 | 138 | async def step(self, action): 139 | ctx = action['ctx'] 140 | content = action['content'] 141 | 142 | forced_reward = None 143 | # take content 144 | try: 145 | created_orders = await self.trade_function(ctx, content) 146 | except octobot_trading_errors.PortfolioNegativeValueError: 147 | forced_reward = -1 148 | 149 | self._idx += 1 150 | self._step += 1 151 | 152 | done, truncated = False, False 153 | 154 | if not done and forced_reward is None: 155 | current_pf_value = get_current_portfolio_value(ctx) 156 | profitabilities = get_profitabilities(ctx) 157 | current_profitability = profitabilities[1] 158 | market_profitability = profitabilities[3] 159 | reward = self.reward_function(current_pf_value, 160 | self._previous_portfolio_value, 161 | current_profitability, 162 | market_profitability, 163 | created_orders) 164 | self._previous_portfolio_value = current_pf_value 165 | else: 166 | reward = forced_reward 167 | # TODO save reward 168 | 169 | if done or truncated: 170 | # TODO ? 171 | None 172 | return await self.get_obs(ctx), reward, done, truncated 173 | -------------------------------------------------------------------------------- /octobot_script/ai/models.py: -------------------------------------------------------------------------------- 1 | from keras.models import Sequential 2 | from keras.layers import Dense 3 | from keras.optimizers import Adam 4 | 5 | 6 | def mlp(n_action, n_hidden_layer=1, n_neuron_per_layer=32, 7 | activation='relu', loss='mse'): 8 | """ A multi-layer perceptron """ 9 | print(n_action) 10 | model = Sequential() 11 | 12 | model.add(Dense(n_neuron_per_layer, input_dim=1, activation=activation)) 13 | for _ in range(n_hidden_layer): 14 | model.add(Dense(n_neuron_per_layer, activation=activation)) 15 | model.add(Dense(n_action, activation='relu')) 16 | model.compile(loss=loss, optimizer=Adam()) 17 | print(model.summary()) 18 | return model 19 | -------------------------------------------------------------------------------- /octobot_script/api/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | 18 | from octobot_script.api.data_fetching import * 19 | from octobot_script.api.execution import * 20 | from octobot_script.api.ploting import * 21 | -------------------------------------------------------------------------------- /octobot_script/api/data_fetching.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import octobot_backtesting.api as backtesting_api 18 | import octobot_commons.symbols as commons_symbols 19 | import octobot_commons.enums as commons_enums 20 | import octobot_trading.enums as trading_enums 21 | import octobot_script.internal.octobot_mocks as octobot_mocks 22 | 23 | 24 | def _ensure_ms_timestamp(timestamp): 25 | if timestamp is None: 26 | return timestamp 27 | if timestamp < 16737955050: # Friday 28 May 2500 07:57:30 28 | return timestamp * 1000 29 | 30 | 31 | async def historical_data(symbol, timeframe, exchange="binance", exchange_type=trading_enums.ExchangeTypes.SPOT.value, 32 | start_timestamp=None, end_timestamp=None): 33 | symbols = [symbol] 34 | time_frames = [commons_enums.TimeFrames(timeframe)] 35 | data_collector_instance = backtesting_api.exchange_historical_data_collector_factory( 36 | exchange, 37 | trading_enums.ExchangeTypes(exchange_type), 38 | octobot_mocks.get_tentacles_config(), 39 | [commons_symbols.parse_symbol(symbol) for symbol in symbols], 40 | time_frames=time_frames, 41 | start_timestamp=_ensure_ms_timestamp(start_timestamp), 42 | end_timestamp=_ensure_ms_timestamp(end_timestamp) 43 | ) 44 | return await backtesting_api.initialize_and_run_data_collector(data_collector_instance) 45 | 46 | 47 | async def get_data(symbol, time_frame, exchange="binance", exchange_type=trading_enums.ExchangeTypes.SPOT.value, 48 | start_timestamp=None, end_timestamp=None, data_file=None): 49 | data = data_file or \ 50 | await historical_data(symbol, timeframe=time_frame, exchange=exchange, exchange_type=exchange_type, 51 | start_timestamp=start_timestamp, end_timestamp=end_timestamp) 52 | return await backtesting_api.create_and_init_backtest_data( 53 | [data], 54 | octobot_mocks.get_config(), 55 | octobot_mocks.get_tentacles_config(), 56 | use_accurate_price_time_frame=True 57 | ) 58 | -------------------------------------------------------------------------------- /octobot_script/api/execution.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import octobot_script.internal.logging_util as logging_util 18 | import octobot_script.internal.runners as runners 19 | 20 | 21 | async def run(backtesting_data, update_func, strategy_config, 22 | enable_logs=False, enable_storage=True): 23 | if enable_logs: 24 | logging_util.load_logging_config() 25 | return await runners.run( 26 | backtesting_data, update_func, strategy_config, 27 | enable_logs=enable_logs, enable_storage=enable_storage 28 | ) 29 | -------------------------------------------------------------------------------- /octobot_script/api/ploting.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=E1101 2 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 3 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 4 | # 5 | # OctoBot is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either 8 | # version 3.0 of the License, or (at your option) any later version. 9 | # 10 | # OctoBot is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public 16 | # License along with OctoBot-Script. If not, see . 17 | 18 | async def plot_indicator(ctx, name, x, y, signals=None): 19 | # lazy import 20 | import octobot_script as obs 21 | 22 | await obs.plot(ctx, name, x=list(x), y=list(y)) 23 | value_by_x = { 24 | x: y 25 | for x, y in zip(x, y) 26 | } 27 | if signals: 28 | await obs.plot(ctx, "signals", x=list(signals), y=[value_by_x[x] for x in signals], mode="markers") 29 | 30 | -------------------------------------------------------------------------------- /octobot_script/cli.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | 18 | import click 19 | import asyncio 20 | import aiohttp 21 | import sys 22 | 23 | import octobot_script 24 | import octobot_script.internal.octobot_mocks as octobot_mocks 25 | import octobot_script.internal.logging_util as octobot_script_logging 26 | import octobot_tentacles_manager.api as api 27 | 28 | 29 | async def install_all_tentacles(quite_mode) -> bool: 30 | octobot_script_logging.enable_base_logger() 31 | error_count = 0 32 | install_path = octobot_mocks.get_module_appdir_path() 33 | tentacles_path = octobot_mocks.get_tentacles_path() 34 | tentacles_urls = octobot_mocks.get_public_tentacles_urls() 35 | async with aiohttp.ClientSession() as aiohttp_session: 36 | for tentacles_url in tentacles_urls: 37 | error_count += await api.install_all_tentacles(tentacles_url, 38 | tentacle_path=tentacles_path, 39 | bot_path=install_path, 40 | aiohttp_session=aiohttp_session, 41 | quite_mode=quite_mode, 42 | bot_install_dir=install_path) 43 | return error_count == 0 44 | 45 | 46 | @click.group 47 | @click.version_option(version=octobot_script.VERSION, prog_name=octobot_script.PROJECT_NAME) 48 | def main(): 49 | """ 50 | OctoBot-Script command line interface. 51 | """ 52 | 53 | 54 | @main.command("install_tentacles") 55 | @click.option('--quite', flag_value=True, help='Only display errors in logs.') 56 | def sync_install_tentacles(quite): 57 | """ 58 | (Re)-install the available OctoBot tentacles. 59 | """ 60 | sys.exit(0 if asyncio.run(install_all_tentacles(quite)) else -1) 61 | 62 | 63 | if __name__ == "__main__": 64 | main() 65 | -------------------------------------------------------------------------------- /octobot_script/config/config_mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "backtesting": { 3 | "files": [] 4 | }, 5 | "exchanges": {}, 6 | "services": { 7 | "web": { 8 | "auto-open-in-web-browser": true 9 | } 10 | }, 11 | "trader": { 12 | "enabled": false, 13 | "load-trade-history": false 14 | }, 15 | "trader-simulator": { 16 | "enabled": true, 17 | "fees": { 18 | "maker": 0.01, 19 | "taker": 0.06 20 | }, 21 | "starting-portfolio": { 22 | "BTC": 0, 23 | "ETH": 0, 24 | "USD": 0, 25 | "USDT": 10000 26 | } 27 | }, 28 | "trading": { 29 | "reference-market": "USDT", 30 | "risk": 0.5 31 | }, 32 | "notification":{ 33 | "global-info": true, 34 | "price-alerts": true, 35 | "trades": true, 36 | "trading-script-alerts": true, 37 | "other": true, 38 | "notification-type": [ 39 | "web" 40 | ] 41 | }, 42 | "profile": "daily_trading", 43 | "accepted_terms": false 44 | } 45 | -------------------------------------------------------------------------------- /octobot_script/config/logging_config.ini: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [handlers] 5 | keys=consoleHandler,fileHandler 6 | 7 | [formatters] 8 | keys=consoleFormatter,fileFormatter 9 | 10 | [logger_root] 11 | level=DEBUG 12 | handlers=consoleHandler,fileHandler 13 | 14 | [handler_consoleHandler] 15 | class=StreamHandler 16 | level=DEBUG 17 | formatter=consoleFormatter 18 | args=(sys.stdout,) 19 | 20 | [handler_fileHandler] 21 | class=handlers.RotatingFileHandler 22 | level=DEBUG 23 | formatter=fileFormatter 24 | args=('logs/OctoBot.log', 'a', 24000000, 20) 25 | 26 | [formatter_consoleFormatter] 27 | class=colorlog.ColoredFormatter 28 | format=%(log_color)s %(asctime)s %(levelname)-8s %(name)-20s %(message)s 29 | 30 | [formatter_fileFormatter] 31 | format=%(asctime)-16s %(levelname)-6s %(name)-20s %(filename)-s:%(lineno)-8s %(message)s 32 | datefmt=%Y-%m-%d %H:%M:%S 33 | -------------------------------------------------------------------------------- /octobot_script/constants.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | 18 | ADDITIONAL_IMPORT_PATH = "imports" 19 | CONFIG_PATH = "config" 20 | -------------------------------------------------------------------------------- /octobot_script/internal/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | -------------------------------------------------------------------------------- /octobot_script/internal/backtester_trading_mode.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | # import API first to avoid import issues 18 | import octobot_trading.api # pylint: disable=unused-import 19 | import octobot_trading.modes as modes 20 | import octobot_trading.enums as trading_enums 21 | 22 | 23 | class BacktesterTradingMode(modes.AbstractScriptedTradingMode): 24 | 25 | def __init__(self, config, exchange_manager): 26 | super().__init__(config, exchange_manager) 27 | self._import_scripts() 28 | 29 | def _import_scripts(self): 30 | pass 31 | 32 | @classmethod 33 | def get_supported_exchange_types(cls) -> list: 34 | """ 35 | :return: The list of supported exchange types 36 | """ 37 | return [ 38 | trading_enums.ExchangeTypes.SPOT, 39 | trading_enums.ExchangeTypes.FUTURE, 40 | trading_enums.ExchangeTypes.MARGIN, 41 | ] 42 | -------------------------------------------------------------------------------- /octobot_script/internal/logging_util.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import logging 18 | import logging.config as config 19 | import os.path 20 | 21 | import octobot.logger 22 | import octobot_script.internal.octobot_mocks as octobot_mocks 23 | 24 | 25 | def load_logging_config(config_file="logging_config.ini"): 26 | if octobot.logger.BOT_CHANNEL_LOGGER is not None: 27 | # logs already initialized 28 | return 29 | logs_folder = "logs" 30 | if not os.path.exists(logs_folder): 31 | os.mkdir(logs_folder) 32 | try: 33 | config.fileConfig(config_file) 34 | except KeyError: 35 | logging_config = os.path.join(octobot_mocks.get_module_install_path(), "config", config_file) 36 | config.fileConfig(logging_config) 37 | octobot.logger.init_bot_channel_logger() 38 | 39 | 40 | def enable_base_logger(): 41 | logging.basicConfig( 42 | level=logging.DEBUG, 43 | datefmt="%Y-%m-%d %H:%M:%S", 44 | format="%(asctime)s %(levelname)-8s %(name)-20s %(message)s" 45 | ) 46 | 47 | -------------------------------------------------------------------------------- /octobot_script/internal/octobot_mocks.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import os 18 | import json 19 | import appdirs 20 | 21 | import octobot_script 22 | import octobot_script.constants as constants 23 | import octobot_script.internal.backtester_trading_mode 24 | import octobot_commons.constants as commons_constants 25 | import octobot_tentacles_manager.api as octobot_tentacles_manager_api 26 | import octobot_tentacles_manager.constants as octobot_tentacles_manager_constants 27 | import octobot.configuration_manager as octobot_configuration_manager 28 | 29 | 30 | def get_tentacles_config(): 31 | # use tentacles config from user appdirs as it is kept up to date at each tentacle packages install 32 | ref_tentacles_config_path = os.path.join( 33 | get_module_appdir_path(), 34 | octobot_tentacles_manager_constants.USER_REFERENCE_TENTACLE_CONFIG_PATH, 35 | commons_constants.CONFIG_TENTACLES_FILE 36 | ) 37 | tentacles_setup_config = octobot_tentacles_manager_api.get_tentacles_setup_config(ref_tentacles_config_path) 38 | # activate OctoBot-Script required tentacles 39 | _force_tentacles_config_activation(tentacles_setup_config) 40 | return tentacles_setup_config 41 | 42 | 43 | def get_config(): 44 | with open(get_module_config_path("config_mock.json")) as f: 45 | return json.load(f) 46 | 47 | 48 | def get_module_install_path(): 49 | return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 50 | 51 | 52 | def get_module_config_path(file_name): 53 | return os.path.join(get_module_install_path(), constants.CONFIG_PATH, file_name) 54 | 55 | 56 | def get_module_appdir_path(): 57 | dirs = appdirs.AppDirs(octobot_script.PROJECT_NAME, octobot_script.AUTHOR, octobot_script.VERSION) 58 | return dirs.user_data_dir 59 | 60 | 61 | def get_internal_import_path(): 62 | return os.path.join(get_module_appdir_path(), constants.ADDITIONAL_IMPORT_PATH) 63 | 64 | 65 | def get_tentacles_path(): 66 | return os.path.join(get_internal_import_path(), octobot_tentacles_manager_constants.TENTACLES_PATH) 67 | 68 | 69 | def get_imported_tentacles_path(): 70 | import tentacles 71 | return os.path.dirname(os.path.abspath(tentacles.__file__)) 72 | 73 | 74 | def get_public_tentacles_urls(): 75 | return [ 76 | octobot_configuration_manager.get_default_tentacles_url() 77 | ] 78 | 79 | 80 | def _force_tentacles_config_activation(tentacles_setup_config): 81 | import tentacles.Evaluator 82 | forced_tentacles = { 83 | octobot_tentacles_manager_constants.TENTACLES_EVALUATOR_PATH: { 84 | tentacles.Evaluator.BlankStrategyEvaluator.get_name(): True 85 | }, 86 | octobot_tentacles_manager_constants.TENTACLES_TRADING_PATH: { 87 | octobot_script.internal.backtester_trading_mode.BacktesterTradingMode.get_name(): True 88 | } 89 | } 90 | for topic, activations in forced_tentacles.items(): 91 | for tentacle, activated in activations.items(): 92 | tentacles_setup_config.tentacles_activation[topic][tentacle] = activated 93 | -------------------------------------------------------------------------------- /octobot_script/internal/runners.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import octobot.api as octobot_api 18 | import octobot_backtesting.api as backtesting_api 19 | import octobot_commons.constants as commons_constants 20 | import octobot_commons.enums as commons_enums 21 | 22 | import octobot_script.model as models 23 | import octobot_script.internal.backtester_trading_mode as backtester_trading_mode 24 | 25 | 26 | async def run(backtesting_data, update_func, strategy_config, 27 | enable_logs=False, enable_storage=False): 28 | backtest_result = models.BacktestResult(backtesting_data, strategy_config) 29 | _register_strategy(update_func, strategy_config) 30 | independent_backtesting = octobot_api.create_independent_backtesting( 31 | backtesting_data.config, 32 | backtesting_data.tentacles_config, 33 | backtesting_data.data_files, 34 | run_on_common_part_only=True, 35 | start_timestamp=None, 36 | end_timestamp=None, 37 | enable_logs=enable_logs, 38 | stop_when_finished=False, 39 | run_on_all_available_time_frames=True, 40 | enforce_total_databases_max_size_after_run=False, 41 | enable_storage=enable_storage, 42 | backtesting_data=backtesting_data, 43 | ) 44 | await octobot_api.initialize_and_run_independent_backtesting(independent_backtesting) 45 | await independent_backtesting.join_backtesting_updater(None) 46 | await _gather_results(independent_backtesting, backtest_result) 47 | await octobot_api.stop_independent_backtesting(independent_backtesting) 48 | return backtest_result 49 | 50 | 51 | async def _gather_results(independent_backtesting, backtest_result): 52 | backtest_result.independent_backtesting = independent_backtesting 53 | backtest_result.duration = backtesting_api.get_backtesting_duration( 54 | independent_backtesting.octobot_backtesting.backtesting 55 | ) 56 | backtest_result.candles_count = sum( 57 | candle_manager.get_preloaded_symbol_candles_count() 58 | for candle_manager in backtest_result.backtesting_data.preloaded_candle_managers.values() 59 | ) 60 | backtest_result.report = await independent_backtesting.get_dict_formatted_report() 61 | backtest_result.bot_id = independent_backtesting.octobot_backtesting.bot_id 62 | 63 | 64 | def _register_strategy(update_func, strategy_config): 65 | def _local_import_scripts(self, *args): 66 | self._live_script = update_func 67 | original_reload_config = self.reload_config 68 | 69 | async def _local_reload_config(*args, **kwargs): 70 | await original_reload_config(*args, **kwargs) 71 | updated_config = { 72 | commons_constants.CONFIG_ACTIVATION_TOPICS.replace(" ", "_"): 73 | commons_enums.ActivationTopics.FULL_CANDLES.value 74 | } 75 | updated_config.update(strategy_config) 76 | self.trading_config.update(updated_config) 77 | self.reload_config = _local_reload_config 78 | backtester_trading_mode.BacktesterTradingMode._import_scripts = _local_import_scripts 79 | -------------------------------------------------------------------------------- /octobot_script/model/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | from octobot_script.model.strategy import * 18 | from octobot_script.model.backtest_result import * 19 | -------------------------------------------------------------------------------- /octobot_script/model/backtest_plot.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | import time 17 | import webbrowser 18 | import jinja2 19 | import json 20 | import http.server 21 | import socketserver 22 | 23 | import octobot_commons.constants as commons_constants 24 | import octobot_commons.display as display 25 | import octobot_commons.logging as logging 26 | import octobot_commons.timestamp_util as timestamp_util 27 | import octobot.api as octobot_api 28 | import octobot_script.resources as resources 29 | import octobot_script.internal.backtester_trading_mode as backtester_trading_mode 30 | 31 | 32 | class BacktestPlot: 33 | DEFAULT_REPORT_NAME = "report.html" 34 | DEFAULT_TEMPLATE = "default_report_template.html" 35 | JINJA_ENVIRONMENT = jinja2.Environment(loader=jinja2.FileSystemLoader( 36 | resources.get_report_resource_path(None) 37 | )) 38 | GENERATED_TIME_FORMAT = "%Y-%m-%d at %H:%M:%S" 39 | SERVER_PORT = 5555 40 | SERVER_HOST = "localhost" 41 | 42 | def __init__(self, backtest_result, run_db_identifier, report_file=None): 43 | self.backtest_result = backtest_result 44 | self.report_file = report_file 45 | self.run_db_identifier = run_db_identifier 46 | self.backtesting_analysis_settings = self.default_backtesting_analysis_settings() 47 | 48 | async def fill(self, template_file=None): 49 | template = self.JINJA_ENVIRONMENT.get_template(template_file or self.DEFAULT_TEMPLATE) 50 | template_data = await self._get_template_data() 51 | with open(self.report_file, "w") as report: 52 | report.write(template.render(template_data)) 53 | 54 | def show(self): 55 | backtest_plot_instance = self 56 | print(f"Report in {self.report_file}") 57 | 58 | class ReportRequestHandler(http.server.SimpleHTTPRequestHandler): 59 | def log_request(self, *_, **__): 60 | # do not log requests 61 | pass 62 | 63 | def do_GET(self): 64 | self.send_response(http.HTTPStatus.OK) 65 | self.send_header("Content-type", "text/html") 66 | self.end_headers() 67 | 68 | with open(backtest_plot_instance.report_file, "rb") as report: 69 | self.wfile.write(report.read()) 70 | 71 | try: 72 | with socketserver.TCPServer(("", self.SERVER_PORT), ReportRequestHandler) as httpd: 73 | webbrowser.open(f"http://{self.SERVER_HOST}:{self.SERVER_PORT}") 74 | httpd.handle_request() 75 | except Exception: 76 | webbrowser.open(self.report_file) 77 | 78 | async def _get_template_data(self): 79 | full_data, symbols, time_frames, exchanges = await self._get_full_data() 80 | return { 81 | "FULL_DATA": full_data, 82 | "title": f"{', '.join(symbols)}", 83 | "top_title": f"{', '.join(symbols)} on {', '.join(time_frames)} from " 84 | f"{', '.join([e.capitalize() for e in exchanges])}", 85 | "creation_time": timestamp_util.convert_timestamp_to_datetime(time.time(), self.GENERATED_TIME_FORMAT), 86 | "middle_title": "Portfolio value", 87 | "bottom_title": "Details", 88 | "strategy_config": self.backtest_result.strategy_config 89 | } 90 | 91 | async def _get_full_data(self): 92 | # tentacles not available during first install 93 | import tentacles.Meta.Keywords.scripting_library as scripting_library 94 | elements = display.display_translator_factory() 95 | trading_mode = backtester_trading_mode.BacktesterTradingMode 96 | symbols = [] 97 | time_frames = [] 98 | exchanges = [] 99 | for exchange, available_symbols in octobot_api.get_independent_backtesting_symbols_by_exchanges( 100 | self.backtest_result.independent_backtesting 101 | ).items(): 102 | exchanges.append(exchange) 103 | for symbol in available_symbols: 104 | symbol = str(symbol) 105 | symbols.append(symbol) 106 | for time_frame in octobot_api.get_independent_backtesting_config( 107 | self.backtest_result.independent_backtesting)[commons_constants.CONFIG_TIME_FRAME]: 108 | time_frames.append(time_frame.value) 109 | await elements.fill_from_database( 110 | trading_mode, self.run_db_identifier, exchange, symbol, time_frame.value, 111 | None, with_inputs=False 112 | ) 113 | ctx = scripting_library.Context.minimal( 114 | trading_mode, logging.get_logger(self.__class__.__name__), exchange, symbol, 115 | self.run_db_identifier.backtesting_id, self.run_db_identifier.optimizer_id, 116 | self.run_db_identifier.optimization_campaign_name, self.backtesting_analysis_settings) 117 | elements.add_parts_from_other(await scripting_library.default_backtesting_analysis_script(ctx)) 118 | return json.dumps(elements.to_json()), symbols, time_frames, exchanges 119 | 120 | def default_backtesting_analysis_settings(self): 121 | return { 122 | "display_backtest_details": True, 123 | "display_trades_and_positions": True, 124 | "plot_best_case_growth_on_backtesting_chart": False, 125 | "plot_funding_fees_on_backtesting_chart": False, 126 | "plot_hist_portfolio_on_backtesting_chart": True, 127 | "plot_pnl_on_backtesting_chart": False, 128 | "plot_pnl_on_main_chart": False, 129 | "plot_trade_gains_on_backtesting_chart": False, 130 | "plot_trade_gains_on_main_chart": False, 131 | "plot_win_rate_on_backtesting_chart": False, 132 | "plot_wins_and_losses_count_on_backtesting_chart": False, 133 | "display_backtest_details_general": False, 134 | "display_backtest_details_performances": True, 135 | "display_backtest_details_details": False, 136 | "display_backtest_details_strategy_settings": False, 137 | } 138 | -------------------------------------------------------------------------------- /octobot_script/model/backtest_result.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import os 18 | import octobot_script.model.backtest_plot as backtest_plot 19 | import octobot_script.model.errors as errors 20 | import octobot_commons.databases as commons_databases 21 | 22 | 23 | class BacktestResult: 24 | def __init__(self, backtesting_data, strategy_config): 25 | self.backtesting_data = backtesting_data 26 | self.strategy_config = strategy_config 27 | self.independent_backtesting = None 28 | self.duration = None 29 | self.candles_count = None 30 | self.report = {} 31 | self.bot_id = None 32 | 33 | def describe(self): 34 | return f"[{round(self.duration, 3)}s / {self.candles_count} candles] profitability: {self.report['bot_report']['profitability']} " \ 35 | f"market average: {self.report['bot_report']['market_average_profitability']} " \ 36 | f"strategy_config: {self.strategy_config}" 37 | 38 | async def plot(self, report_file=None, show=False): 39 | if not commons_databases.RunDatabasesProvider.instance().is_storage_enabled(self.bot_id): 40 | raise errors.ParameterError("storage has to be enabled to plot backtesting data") 41 | plot_result = await self._get_plotted_result(report_file=report_file) 42 | if show: 43 | plot_result.show() 44 | return plot_result 45 | 46 | async def _get_plotted_result(self, report_file=None): 47 | run_db_id = commons_databases.RunDatabasesProvider.instance().get_run_databases_identifier(self.bot_id) 48 | plot = backtest_plot.BacktestPlot( 49 | self, run_db_id, report_file=report_file or self.get_default_plotted_report_file(run_db_id) 50 | ) 51 | await plot.fill() 52 | return plot 53 | 54 | def get_default_plotted_report_file(self, run_db_id): 55 | return os.path.join( 56 | run_db_id.get_backtesting_run_folder(), 57 | backtest_plot.BacktestPlot.DEFAULT_REPORT_NAME 58 | ) 59 | -------------------------------------------------------------------------------- /octobot_script/model/errors.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | class ParameterError(Exception): 18 | pass 19 | -------------------------------------------------------------------------------- /octobot_script/model/strategy.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | class Strategy: 18 | def __init__(self, config=None): 19 | self.config = config 20 | -------------------------------------------------------------------------------- /octobot_script/resources/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import os 18 | 19 | 20 | def get_report_resource_path(resource_name): 21 | base_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "reports") 22 | if resource_name: 23 | return os.path.join(base_path, resource_name) 24 | return base_path 25 | -------------------------------------------------------------------------------- /octobot_script/resources/reports/css/style.css: -------------------------------------------------------------------------------- 1 | /** colors **/ 2 | :root { 3 | --bg: #131722; 4 | --fnt: #b2b5be; 5 | --brdr: #2a2e39; 6 | --brdr-actv: #B71C1C; 7 | --fnt-hvr: #131722; 8 | --bg-hvr: #C62828; 9 | --fnt-actv: #B71C1C; 10 | --new-warning: #fb3; 11 | --new-success: #00c851; 12 | } 13 | 14 | .bg-dark, .form-control options, ul { 15 | background: var(--bg) !important; 16 | color: var(--fnt) !important; 17 | } 18 | 19 | .octobot-logo { 20 | height: 40px; 21 | } 22 | 23 | /** plotting **/ 24 | 25 | .hoverlayer line:first-child, .hoverlayer line:nth-child(3) { 26 | /*hide selector borders*/ 27 | stroke: unset; 28 | } 29 | 30 | .main-chart { 31 | height: 300px; 32 | } 33 | 34 | .sub-chart { 35 | height: 200px; 36 | } 37 | 38 | .sub-chart { 39 | height: 200px; 40 | } 41 | 42 | .backtesting-run-overview{ 43 | height: 200px; 44 | } 45 | -------------------------------------------------------------------------------- /octobot_script/resources/reports/css/w2ui_template.css: -------------------------------------------------------------------------------- 1 | /*global*/ 2 | .w2ui-empty-record, .w2ui-empty-record:hover, .w2ui-grid-footer, .w2ui-grid-columns, 3 | w2ui-grid-columns *, .w2ui-grid-toolbar,.w2ui-grid-toolbar *, .w2ui-grid-header, .w2ui-record, 4 | .grid_RSI_footer, .w2ui-overlay *, .w2ui-message *{ 5 | background-color: var(--brdr) !important; 6 | color: var(--fnt) !important; 7 | background-image: unset !important; 8 | } 9 | 10 | /*searches*/ 11 | .w2ui-head, .w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-search-all, 12 | .w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-search-all:focus { 13 | background-color: var(--bg) !important; 14 | color: var(--fnt) !important; 15 | background-image: unset !important; 16 | } 17 | 18 | /*rows*/ 19 | .w2ui-even{ 20 | background-color: var(--bg) !important; 21 | } 22 | 23 | .w2ui-odd{ 24 | background-color: var(--brdr) !important; 25 | } 26 | 27 | /*hover*/ 28 | .w2ui-record:hover { 29 | background-color: var(--bg-hvr) !important; 30 | } 31 | 32 | /*selected*/ 33 | .w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-inactive, 34 | .w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-selected { 35 | background-color: var(--brdr-actv) !important; 36 | color: var(--fnt) !important; 37 | } 38 | -------------------------------------------------------------------------------- /octobot_script/resources/reports/default_report_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include 'header.html' %} 4 | 5 |
6 |

7 | 8 | {{ title }} 9 | 10 | OctoBot Script 11 | 12 | report 13 |

14 |
15 |
16 |

17 | {{ top_title }} 18 | Generated the {{ creation_time }} 19 |

20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |

29 | {{ middle_title }} 30 |

31 |
32 |
33 |
34 |
35 |
36 |

37 | {{ bottom_title }} 38 |

39 |
40 |
41 |

42 | Configuration 43 |

44 |
45 | {% for key, val in strategy_config.items() %} 46 |
47 | {{key}}: {{val}} 48 |
49 | {% endfor %} 50 |
51 |
52 |
53 |
54 |
55 |
56 | 57 | 58 | {% include 'scripts.html' %} 59 | 60 | -------------------------------------------------------------------------------- /octobot_script/resources/reports/header.html: -------------------------------------------------------------------------------- 1 | 2 | {{ title }} OctoBot Script report 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 31 | 32 | 36 | -------------------------------------------------------------------------------- /octobot_script/resources/reports/js/common.js: -------------------------------------------------------------------------------- 1 | const hidden_class = "d-none"; 2 | const borderColor = "#2a2e39"; 3 | const textColor = "#b2b5be"; 4 | -------------------------------------------------------------------------------- /octobot_script/resources/reports/js/data.js: -------------------------------------------------------------------------------- 1 | const FULL_DATA = {{ FULL_DATA }}.data.sub_elements; 2 | -------------------------------------------------------------------------------- /octobot_script/resources/reports/js/graphs.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | const CANDLES_PLOT_SOURCES = ["open", "high", "low", "close"]; 4 | const ALL_PLOT_SOURCES = ["y", "z", "volume"].concat(CANDLES_PLOT_SOURCES); 5 | 6 | const getPlotlyConfig = () => { 7 | return { 8 | scrollZoom: true, 9 | modeBarButtonsToRemove: ["select2d", "lasso2d", "toggleSpikelines"], 10 | responsive: true, 11 | showEditInChartStudio: false, 12 | displaylogo: false // no logo to avoid 'rel="noopener noreferrer"' security issue (see https://webhint.io/docs/user-guide/hints/hint-disown-opener/) 13 | }; 14 | } 15 | 16 | const _getChartedElements = (chartDetails, yAxis, xAxis, chartIdentifier, plotOnlyY) => { 17 | const chartedElements = { 18 | x: chartDetails.x, 19 | mode: chartDetails.mode, 20 | type: chartDetails.kind, 21 | text: chartDetails.text, 22 | name: `${chartDetails.title}${chartIdentifier}`, 23 | user_title: chartDetails.title, 24 | } 25 | chartedElements.line = { 26 | shape: chartDetails.line_shape 27 | } 28 | const markerAttributes = ["color", "size", "opacity", "line", "symbol"]; 29 | chartedElements.marker = {}; 30 | markerAttributes.forEach(function (attribute) { 31 | if (chartDetails[attribute] !== null) { 32 | chartedElements.marker[attribute] = chartDetails[attribute]; 33 | } 34 | }); 35 | if (plotOnlyY) { 36 | chartedElements.y = chartDetails.y; 37 | } else { 38 | ALL_PLOT_SOURCES.forEach(function (element) { 39 | if (chartDetails[element] !== null) { 40 | chartedElements[element] = chartDetails[element] 41 | } 42 | }) 43 | } 44 | if (xAxis > 1) { 45 | chartedElements.xaxis = `x${xAxis}` 46 | } 47 | chartedElements.yaxis = `y${yAxis}` 48 | return chartedElements; 49 | } 50 | 51 | const _createOrAdaptChartedElements = (chartDetails, yAxis, xAxis, chartIdentifier) => { 52 | const createdChartedElements = []; 53 | if(chartDetails.x === null){ 54 | return createdChartedElements; 55 | } 56 | createdChartedElements.push( 57 | _getChartedElements(chartDetails, yAxis, xAxis, chartIdentifier, false) 58 | ); 59 | return createdChartedElements; 60 | } 61 | 62 | const createChartLayout = (chartDetails, chartData, yAxis, xAxis, xaxis_list, yaxis_list, chartIdentifier) => { 63 | const xaxis = { 64 | gridcolor: borderColor, 65 | color: textColor, 66 | autorange: true, 67 | rangeslider: { 68 | visible: false, 69 | }, 70 | domain: [0.02, 0.98] 71 | }; 72 | const yaxis = { 73 | }; 74 | if(chartDetails.x_type !== null){ 75 | xaxis.type = chartDetails.x_type; 76 | } 77 | if(xAxis > 1){ 78 | xaxis.overlaying = "x" 79 | } 80 | _createOrAdaptChartedElements(chartDetails, yAxis, xAxis, chartIdentifier).forEach(element => { 81 | chartData.push(element); 82 | }) 83 | const layout = { 84 | autosize: true, 85 | margin: {l: 50, r: 50, b: 30, t: 0, pad: 0}, 86 | showlegend: true, 87 | legend: {x: 0.01, xanchor: 'left', y: 0.99, yanchor:"top"}, 88 | paper_bgcolor: 'rgba(0,0,0,0)', 89 | plot_bgcolor: 'rgba(0,0,0,0)', 90 | dragmode: "pan", 91 | font: { 92 | color: textColor 93 | }, 94 | yaxis: { 95 | gridcolor: borderColor, 96 | color: textColor, 97 | fixedrange: false, 98 | anchor: "free", 99 | overlaying: "y", 100 | side: 'left', 101 | position: 0 102 | }, 103 | yaxis2: { 104 | gridcolor: borderColor, 105 | color: textColor, 106 | fixedrange: false, 107 | anchor: "free", 108 | overlaying: "y", 109 | side: 'right', 110 | position: 1 111 | }, 112 | yaxis3: { 113 | gridcolor: borderColor, 114 | color: textColor, 115 | fixedrange: false, 116 | anchor: "free", 117 | overlaying: "y", 118 | side: 'right', 119 | position: 0.985 120 | }, 121 | yaxis4: { 122 | gridcolor: borderColor, 123 | color: textColor, 124 | fixedrange: false, 125 | anchor: "free", 126 | overlaying: "y", 127 | side: 'left', 128 | position: 0.015 129 | } 130 | }; 131 | if(true){ 132 | // unified tooltip 133 | layout.hovermode = "x unified"; 134 | layout.hoverlabel = { 135 | bgcolor: "#131722", 136 | bordercolor: borderColor 137 | }; 138 | } else { 139 | layout.hovermode = false; 140 | } 141 | 142 | const MAX_AXIS_INDEX = 4; 143 | yaxis_list.push(yaxis) 144 | yaxis_list.forEach(function (axis, i){ 145 | if(i > 0 && i < MAX_AXIS_INDEX){ 146 | layout[`yaxis${i + 1}`].type = axis.type; 147 | } else{ 148 | layout["yaxis"].type = axis.type 149 | } 150 | }); 151 | xaxis_list.push(xaxis) 152 | xaxis_list.forEach(function (axis, i){ 153 | if(i > 0){ 154 | layout[`xaxis${i + 1}`] = axis; 155 | }else{ 156 | layout.xaxis = axis 157 | } 158 | }); 159 | return layout 160 | } 161 | 162 | const createCharts = () => { 163 | FULL_DATA.forEach((maybeChart, index) => { 164 | if (maybeChart.type !== "chart") { 165 | return; 166 | } 167 | const chartData = []; 168 | const xaxis_list = []; 169 | const yaxis_list = []; 170 | let yAxis = 0; 171 | const chartDivID = `${maybeChart.name}-${index}`; 172 | const parentDiv = $(document.getElementById(maybeChart.name)); 173 | parentDiv.append(`
`); 174 | let layout = undefined; 175 | maybeChart.data.elements.forEach((chartDetails) => { 176 | if (chartDetails.own_yaxis && yAxis < 4) { 177 | yAxis += 1; 178 | } else if (yAxis === 0) { 179 | yAxis = 1; 180 | } 181 | let xAxis = 1; 182 | if (chartDetails.own_xaxis) { 183 | xAxis += 1; 184 | } 185 | const chartIdentifier = ""; 186 | layout = createChartLayout(chartDetails, chartData, yAxis, xAxis, xaxis_list, yaxis_list, chartIdentifier); 187 | }) 188 | Plotly.newPlot(chartDivID, chartData, layout, getPlotlyConfig()); 189 | }) 190 | } 191 | 192 | const init = () => { 193 | createCharts(); 194 | } 195 | 196 | init(); 197 | }); 198 | -------------------------------------------------------------------------------- /octobot_script/resources/reports/js/tables.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | const ID_DATA = ["id", "backtesting id", "optimizer id"]; 4 | 5 | const _getTableDataType = (records, search, defaultValue, sampleValue) => { 6 | if (ID_DATA.indexOf(search.field) !== -1){ 7 | return "float"; 8 | } 9 | if (search.type !== null){ 10 | return search.type; 11 | } 12 | const _sampleValue = sampleValue === null ? records[0][search.field] : sampleValue; 13 | if(typeof _sampleValue === "undefined"){ 14 | return defaultValue; 15 | } 16 | if(typeof _sampleValue === "number"){ 17 | return "float"; 18 | } 19 | if(typeof _sampleValue === "string"){ 20 | return "text"; 21 | } 22 | if(typeof _sampleValue === "object"){ 23 | return "list"; 24 | } 25 | return defaultValue; 26 | } 27 | 28 | const _downloadRecords = (name, columns, rows) => { 29 | const columnFields = columns.map((col) => col.field); 30 | let csv = columns.map((col) => col.text).join(",") + "\n"; 31 | csv += rows.map((row) => { 32 | return columnFields.map((field) => { 33 | const value = row[field]; 34 | if(typeof value === "string"){ 35 | return value.replaceAll(",", " "); 36 | } 37 | return value 38 | }).join(",") 39 | }).join("\n"); 40 | const hiddenElement = document.createElement('a'); 41 | hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(csv); 42 | hiddenElement.target = '_blank'; 43 | hiddenElement.download = `${name}.csv`; 44 | hiddenElement.click(); 45 | hiddenElement.remove(); 46 | } 47 | 48 | const _createTable = (elementID, name, tableName, searches, columns, records, columnGroups, searchData, sortData, 49 | selectable, addToTable, reorderRows, deleteRows, onReorderRowCallback, onDeleteCallback) => { 50 | const tableExists = typeof w2ui[tableName] !== "undefined"; 51 | if(tableExists && addToTable){ 52 | w2ui[tableName].add(records) 53 | }else{ 54 | const downloadRecords = () => { 55 | _downloadRecords(name, grid.columns, grid.records); 56 | } 57 | let grid = new w2grid({ 58 | name: tableName, 59 | header: name, 60 | box: `#${elementID}`, 61 | show: { 62 | header: false, 63 | toolbar: true, 64 | footer: true, 65 | toolbarReload: false, 66 | toolbarDelete: deleteRows, 67 | selectColumn: selectable, 68 | orderColumn: reorderRows, 69 | }, 70 | multiSearch: true, 71 | searches: searches, 72 | columns: columns, 73 | records: records, 74 | sortData: sortData, 75 | searchData: searchData, 76 | columnGroups: columnGroups, 77 | reorderRows: reorderRows, 78 | onDelete: onDeleteCallback, 79 | onReorderRow: onReorderRowCallback, 80 | toolbar: { 81 | items: [ 82 | { type: 'button', id: 'exportTable', text: 'Export', icon: "fas fa-file-download" } 83 | ], 84 | onClick(event) { 85 | if (event.target == 'exportTable') { 86 | downloadRecords(); 87 | } 88 | } 89 | } 90 | }); 91 | } 92 | return tableName; 93 | } 94 | 95 | const createTables = () => { 96 | FULL_DATA.forEach((maybeTable) => { 97 | if (maybeTable.type !== "table") { 98 | return; 99 | } 100 | maybeTable.data.elements.forEach((element) => { 101 | const tableName = element.title.replaceAll(" ", "-").replaceAll("*", "-"); 102 | const columns = element.columns.map((col) => { 103 | return { 104 | field: col.field, 105 | text: col.label, 106 | size: `${1 / element.columns.length * 100}%`, 107 | sortable: true, 108 | attr: col.attr, 109 | render: col.render, 110 | } 111 | }); 112 | let startIndex = 0; 113 | const records = element.rows.map((row, index) => { 114 | row.recid = startIndex + index; 115 | return row; 116 | }); 117 | const searches = element.searches.map((search) => { 118 | return { 119 | field: search.field, 120 | label: search.label, 121 | type: _getTableDataType(records, search, "text", null), 122 | options: search.options, 123 | } 124 | }); 125 | const chartDivID = `${maybeTable.name}-${element.title}`; 126 | const parentDiv = $(document.getElementById(maybeTable.name)); 127 | parentDiv.append(`
`); 128 | const tableTitle = element.title.replaceAll("_", " "); 129 | _createTable(chartDivID, tableTitle, tableName, searches, columns, records, [], [], [], 130 | false, true, false, false, null, null); 131 | }); 132 | }); 133 | } 134 | 135 | const init = () => { 136 | createTables(); 137 | } 138 | 139 | init(); 140 | }); 141 | -------------------------------------------------------------------------------- /octobot_script/resources/reports/js/texts.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | const _add_labelled_backtesting_values = (sub_element, parentDiv) => { 4 | parentDiv.append( 5 | `
` 6 | ); 7 | const backtestingValuesGridDiv = parentDiv.find("[data-role='values']"); 8 | backtestingValuesGridDiv.empty(); 9 | sub_element.data.elements.forEach((element) => { 10 | if(element.html === null){ 11 | backtestingValuesGridDiv.append( 12 | `
13 |
${element.title}
14 |
${element.value}
15 |
` 16 | ); 17 | }else{ 18 | backtestingValuesGridDiv.append(element.html); 19 | } 20 | }); 21 | } 22 | 23 | const createTexts = () => { 24 | 25 | FULL_DATA.forEach((maybeValue) => { 26 | if (maybeValue.type !== "value") { 27 | return; 28 | } 29 | const parentDiv = $(document.getElementById(maybeValue.name)); 30 | if(!parentDiv.length){ 31 | return 32 | } 33 | _add_labelled_backtesting_values(maybeValue, parentDiv) 34 | }); 35 | } 36 | 37 | const init = () => { 38 | createTexts(); 39 | } 40 | 41 | init(); 42 | }); 43 | -------------------------------------------------------------------------------- /octobot_script/resources/reports/scripts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /requirements-ai.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | tensorflow==2.11.0 3 | gymnasium 4 | tqdm 5 | tensorboard 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Drakkar-Software requirements 2 | OctoBot==2.0.11 3 | 4 | # manage tentacles install path 5 | appdirs==1.4.4 6 | 7 | # CLI 8 | click==8.1.3 9 | 10 | # versions managed by OctoBot's dependencies 11 | jinja2 12 | aiohttp 13 | 14 | # necessary for pip install 15 | wheel 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | from distutils.command.install import install 6 | 7 | 8 | # from octobot_script import PROJECT_NAME, VERSION 9 | # todo figure out how not to import octobot_script.__init__.py here 10 | PROJECT_NAME = "OctoBot-Script" 11 | VERSION = "0.0.25" # major.minor.revision 12 | 13 | 14 | def _post_install(): 15 | import octobot_script.cli 16 | asyncio.run(octobot_script.cli.install_all_tentacles(True)) 17 | 18 | 19 | class InstallWithPostInstallAction(install): 20 | def run(self): 21 | install.run(self) 22 | self.execute(_post_install, (), msg="Installing OctoBot-Script tentacles") 23 | 24 | 25 | PACKAGES = find_packages( 26 | exclude=[ 27 | "tests", 28 | "octobot_script.imports*", 29 | "octobot_script.user*", 30 | ] 31 | ) 32 | 33 | # long description from README file 34 | with open('README.md', encoding='utf-8') as f: 35 | DESCRIPTION = f.read() 36 | 37 | REQUIRED = open('requirements.txt').readlines() 38 | REQUIRES_PYTHON = '>=3.8' 39 | 40 | setup( 41 | name=PROJECT_NAME, 42 | version=VERSION, 43 | url='https://github.com/Drakkar-Software/OctoBot-Script', 44 | license='GPL-3.0', 45 | author='Drakkar-Software', 46 | author_email='contact@drakkar.software', 47 | description='Backtesting framework of the OctoBot Ecosystem', 48 | packages=PACKAGES, 49 | cmdclass={'install': InstallWithPostInstallAction}, 50 | long_description=DESCRIPTION, 51 | long_description_content_type='text/markdown', 52 | tests_require=["pytest"], 53 | test_suite="tests", 54 | zip_safe=False, 55 | data_files=[], 56 | include_package_data=True, # copy non python files on install 57 | install_requires=REQUIRED, 58 | python_requires=REQUIRES_PYTHON, 59 | entry_points={ 60 | 'console_scripts': [ 61 | 'octobot_script = octobot_script.cli:main' 62 | ] 63 | }, 64 | classifiers=[ 65 | 'Development Status :: 5 - Production/Stable', 66 | 'Operating System :: OS Independent', 67 | 'Operating System :: MacOS :: MacOS X', 68 | 'Operating System :: Microsoft :: Windows', 69 | 'Operating System :: POSIX', 70 | 'Programming Language :: Python :: 3.10', 71 | ], 72 | ) 73 | -------------------------------------------------------------------------------- /standard.rc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Specify a score threshold to be exceeded before program exits with error. 9 | fail-under=10.0 10 | 11 | # Add files or directories to the blacklist. They should be base names, not 12 | # paths. 13 | ignore=CVS 14 | 15 | # Add files or directories matching the regex patterns to the blacklist. The 16 | # regex matches against base names, not paths. 17 | ignore-patterns= 18 | 19 | # Python code to execute, usually for sys.path manipulation such as 20 | # pygtk.require(). 21 | #init-hook= 22 | 23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 24 | # number of processors available to use. 25 | jobs=1 26 | 27 | # Control the amount of potential inferred values when inferring a single 28 | # object. This can help the performance when dealing with large functions or 29 | # complex, nested conditions. 30 | limit-inference-results=100 31 | 32 | # List of plugins (as comma separated values of python module names) to load, 33 | # usually to register additional checkers. 34 | load-plugins= 35 | 36 | # Pickle collected data for later comparisons. 37 | persistent=yes 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape, 142 | import-error, 143 | C, I, R, W # only errors TODO remove 144 | 145 | # Enable the message, report, category or checker with the given id(s). You can 146 | # either give multiple identifier separated by comma (,) or put this option 147 | # multiple time (only on the command line, not in the configuration file where 148 | # it should appear only once). See also the "--disable" option for examples. 149 | enable= 150 | 151 | 152 | [REPORTS] 153 | 154 | # Python expression which should return a score less than or equal to 10. You 155 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 156 | # which contain the number of messages in each category, as well as 'statement' 157 | # which is the total number of statements analyzed. This score is used by the 158 | # global evaluation report (RP0004). 159 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 160 | 161 | # Template used to display messages. This is a python new-style format string 162 | # used to format the message information. See doc for all details. 163 | #msg-template= 164 | 165 | # Set the output format. Available formats are text, parseable, colorized, json 166 | # and msvs (visual studio). You can also give a reporter class, e.g. 167 | # mypackage.mymodule.MyReporterClass. 168 | output-format=text 169 | 170 | # Tells whether to display a full report or only the messages. 171 | reports=no 172 | 173 | # Activate the evaluation score. 174 | score=yes 175 | 176 | 177 | [REFACTORING] 178 | 179 | # Maximum number of nested blocks for function / method body 180 | max-nested-blocks=5 181 | 182 | # Complete name of functions that never returns. When checking for 183 | # inconsistent-return-statements if a never returning function is called then 184 | # it will be considered as an explicit return statement and no message will be 185 | # printed. 186 | never-returning-functions=sys.exit 187 | 188 | 189 | [MISCELLANEOUS] 190 | 191 | # List of note tags to take in consideration, separated by a comma. 192 | notes=FIXME, 193 | XXX, 194 | TODO 195 | 196 | # Regular expression of note tags to take in consideration. 197 | #notes-rgx= 198 | 199 | 200 | [STRING] 201 | 202 | # This flag controls whether inconsistent-quotes generates a warning when the 203 | # character used as a quote delimiter is used inconsistently within a module. 204 | check-quote-consistency=no 205 | 206 | # This flag controls whether the implicit-str-concat should generate a warning 207 | # on implicit string concatenation in sequences defined over several lines. 208 | check-str-concat-over-line-jumps=no 209 | 210 | 211 | [BASIC] 212 | 213 | # Naming style matching correct argument names. 214 | argument-naming-style=snake_case 215 | 216 | # Regular expression matching correct argument names. Overrides argument- 217 | # naming-style. 218 | #argument-rgx= 219 | 220 | # Naming style matching correct attribute names. 221 | attr-naming-style=snake_case 222 | 223 | # Regular expression matching correct attribute names. Overrides attr-naming- 224 | # style. 225 | #attr-rgx= 226 | 227 | # Bad variable names which should always be refused, separated by a comma. 228 | bad-names=foo, 229 | bar, 230 | baz, 231 | toto, 232 | tutu, 233 | tata 234 | 235 | # Bad variable names regexes, separated by a comma. If names match any regex, 236 | # they will always be refused 237 | bad-names-rgxs= 238 | 239 | # Naming style matching correct class attribute names. 240 | class-attribute-naming-style=any 241 | 242 | # Regular expression matching correct class attribute names. Overrides class- 243 | # attribute-naming-style. 244 | #class-attribute-rgx= 245 | 246 | # Naming style matching correct class names. 247 | class-naming-style=PascalCase 248 | 249 | # Regular expression matching correct class names. Overrides class-naming- 250 | # style. 251 | #class-rgx= 252 | 253 | # Naming style matching correct constant names. 254 | const-naming-style=UPPER_CASE 255 | 256 | # Regular expression matching correct constant names. Overrides const-naming- 257 | # style. 258 | #const-rgx= 259 | 260 | # Minimum line length for functions/classes that require docstrings, shorter 261 | # ones are exempt. 262 | docstring-min-length=-1 263 | 264 | # Naming style matching correct function names. 265 | function-naming-style=snake_case 266 | 267 | # Regular expression matching correct function names. Overrides function- 268 | # naming-style. 269 | #function-rgx= 270 | 271 | # Good variable names which should always be accepted, separated by a comma. 272 | good-names=i, 273 | j, 274 | k, 275 | ex, 276 | Run, 277 | _ 278 | 279 | # Good variable names regexes, separated by a comma. If names match any regex, 280 | # they will always be accepted 281 | good-names-rgxs= 282 | 283 | # Include a hint for the correct naming format with invalid-name. 284 | include-naming-hint=no 285 | 286 | # Naming style matching correct inline iteration names. 287 | inlinevar-naming-style=any 288 | 289 | # Regular expression matching correct inline iteration names. Overrides 290 | # inlinevar-naming-style. 291 | #inlinevar-rgx= 292 | 293 | # Naming style matching correct method names. 294 | method-naming-style=snake_case 295 | 296 | # Regular expression matching correct method names. Overrides method-naming- 297 | # style. 298 | #method-rgx= 299 | 300 | # Naming style matching correct module names. 301 | module-naming-style=snake_case 302 | 303 | # Regular expression matching correct module names. Overrides module-naming- 304 | # style. 305 | #module-rgx= 306 | 307 | # Colon-delimited sets of names that determine each other's naming style when 308 | # the name regexes allow several styles. 309 | name-group= 310 | 311 | # Regular expression which should only match function or class names that do 312 | # not require a docstring. 313 | no-docstring-rgx=^_ 314 | 315 | # List of decorators that produce properties, such as abc.abstractproperty. Add 316 | # to this list to register other decorators that produce valid properties. 317 | # These decorators are taken in consideration only for invalid-name. 318 | property-classes=abc.abstractproperty 319 | 320 | # Naming style matching correct variable names. 321 | variable-naming-style=snake_case 322 | 323 | # Regular expression matching correct variable names. Overrides variable- 324 | # naming-style. 325 | #variable-rgx= 326 | 327 | 328 | [SPELLING] 329 | 330 | # Limits count of emitted suggestions for spelling mistakes. 331 | max-spelling-suggestions=4 332 | 333 | # Spelling dictionary name. Available dictionaries: none. To make it work, 334 | # install the python-enchant package. 335 | spelling-dict= 336 | 337 | # List of comma separated words that should not be checked. 338 | spelling-ignore-words= 339 | 340 | # A path to a file that contains the private dictionary; one word per line. 341 | spelling-private-dict-file= 342 | 343 | # Tells whether to store unknown words to the private dictionary (see the 344 | # --spelling-private-dict-file option) instead of raising a message. 345 | spelling-store-unknown-words=no 346 | 347 | 348 | [LOGGING] 349 | 350 | # The type of string formatting that logging methods do. `old` means using % 351 | # formatting, `new` is for `{}` formatting. 352 | logging-format-style=old 353 | 354 | # Logging modules to check that the string format arguments are in logging 355 | # function parameter format. 356 | logging-modules=logging 357 | 358 | 359 | [FORMAT] 360 | 361 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 362 | expected-line-ending-format= 363 | 364 | # Regexp for a line that is allowed to be longer than the limit. 365 | ignore-long-lines=^\s*(# )??$ 366 | 367 | # Number of spaces of indent required inside a hanging or continued line. 368 | indent-after-paren=4 369 | 370 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 371 | # tab). 372 | indent-string=' ' 373 | 374 | # Maximum number of characters on a single line. 375 | max-line-length=100 376 | 377 | # Maximum number of lines in a module. 378 | max-module-lines=1000 379 | 380 | # Allow the body of a class to be on the same line as the declaration if body 381 | # contains single statement. 382 | single-line-class-stmt=no 383 | 384 | # Allow the body of an if to be on the same line as the test if there is no 385 | # else. 386 | single-line-if-stmt=no 387 | 388 | 389 | [TYPECHECK] 390 | 391 | # List of decorators that produce context managers, such as 392 | # contextlib.contextmanager. Add to this list to register other decorators that 393 | # produce valid context managers. 394 | contextmanager-decorators=contextlib.contextmanager 395 | 396 | # List of members which are set dynamically and missed by pylint inference 397 | # system, and so shouldn't trigger E1101 when accessed. Python regular 398 | # expressions are accepted. 399 | generated-members= 400 | 401 | # Tells whether missing members accessed in mixin class should be ignored. A 402 | # mixin class is detected if its name ends with "mixin" (case insensitive). 403 | ignore-mixin-members=yes 404 | 405 | # Tells whether to warn about missing members when the owner of the attribute 406 | # is inferred to be None. 407 | ignore-none=yes 408 | 409 | # This flag controls whether pylint should warn about no-member and similar 410 | # checks whenever an opaque object is returned when inferring. The inference 411 | # can return multiple potential results while evaluating a Python object, but 412 | # some branches might not be evaluated, which results in partial inference. In 413 | # that case, it might be useful to still emit no-member and other checks for 414 | # the rest of the inferred objects. 415 | ignore-on-opaque-inference=yes 416 | 417 | # List of class names for which member attributes should not be checked (useful 418 | # for classes with dynamically set attributes). This supports the use of 419 | # qualified names. 420 | ignored-classes=optparse.Values,thread._local,_thread._local 421 | 422 | # List of module names for which member attributes should not be checked 423 | # (useful for modules/projects where namespaces are manipulated during runtime 424 | # and thus existing member attributes cannot be deduced by static analysis). It 425 | # supports qualified module names, as well as Unix pattern matching. 426 | ignored-modules= 427 | 428 | # Show a hint with possible names when a member name was not found. The aspect 429 | # of finding the hint is based on edit distance. 430 | missing-member-hint=yes 431 | 432 | # The minimum edit distance a name should have in order to be considered a 433 | # similar match for a missing member name. 434 | missing-member-hint-distance=1 435 | 436 | # The total number of similar names that should be taken in consideration when 437 | # showing a hint for a missing member. 438 | missing-member-max-choices=1 439 | 440 | # List of decorators that change the signature of a decorated function. 441 | signature-mutators= 442 | 443 | 444 | [SIMILARITIES] 445 | 446 | # Ignore comments when computing similarities. 447 | ignore-comments=yes 448 | 449 | # Ignore docstrings when computing similarities. 450 | ignore-docstrings=yes 451 | 452 | # Ignore imports when computing similarities. 453 | ignore-imports=no 454 | 455 | # Minimum lines number of a similarity. 456 | min-similarity-lines=4 457 | 458 | 459 | [VARIABLES] 460 | 461 | # List of additional names supposed to be defined in builtins. Remember that 462 | # you should avoid defining new builtins when possible. 463 | additional-builtins= 464 | 465 | # Tells whether unused global variables should be treated as a violation. 466 | allow-global-unused-variables=yes 467 | 468 | # List of strings which can identify a callback function by name. A callback 469 | # name must start or end with one of those strings. 470 | callbacks=cb_, 471 | _cb 472 | 473 | # A regular expression matching the name of dummy variables (i.e. expected to 474 | # not be used). 475 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 476 | 477 | # Argument names that match this expression will be ignored. Default to name 478 | # with leading underscore. 479 | ignored-argument-names=_.*|^ignored_|^unused_ 480 | 481 | # Tells whether we should check for unused import in __init__ files. 482 | init-import=no 483 | 484 | # List of qualified module names which can have objects that can redefine 485 | # builtins. 486 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 487 | 488 | 489 | [DESIGN] 490 | 491 | # Maximum number of arguments for function / method. 492 | max-args=5 493 | 494 | # Maximum number of attributes for a class (see R0902). 495 | max-attributes=7 496 | 497 | # Maximum number of boolean expressions in an if statement (see R0916). 498 | max-bool-expr=5 499 | 500 | # Maximum number of branch for function / method body. 501 | max-branches=12 502 | 503 | # Maximum number of locals for function / method body. 504 | max-locals=15 505 | 506 | # Maximum number of parents for a class (see R0901). 507 | max-parents=7 508 | 509 | # Maximum number of public methods for a class (see R0904). 510 | max-public-methods=20 511 | 512 | # Maximum number of return / yield for function / method body. 513 | max-returns=6 514 | 515 | # Maximum number of statements in function / method body. 516 | max-statements=50 517 | 518 | # Minimum number of public methods for a class (see R0903). 519 | min-public-methods=2 520 | 521 | 522 | [CLASSES] 523 | 524 | # List of method names used to declare (i.e. assign) instance attributes. 525 | defining-attr-methods=__init__, 526 | __new__, 527 | setUp, 528 | __post_init__ 529 | 530 | # List of member names, which should be excluded from the protected access 531 | # warning. 532 | exclude-protected=_asdict, 533 | _fields, 534 | _replace, 535 | _source, 536 | _make 537 | 538 | # List of valid names for the first argument in a class method. 539 | valid-classmethod-first-arg=cls 540 | 541 | # List of valid names for the first argument in a metaclass class method. 542 | valid-metaclass-classmethod-first-arg=cls 543 | 544 | 545 | [IMPORTS] 546 | 547 | # List of modules that can be imported at any level, not just the top level 548 | # one. 549 | allow-any-import-level= 550 | 551 | # Allow wildcard imports from modules that define __all__. 552 | allow-wildcard-with-all=no 553 | 554 | # Analyse import fallback blocks. This can be used to support both Python 2 and 555 | # 3 compatible code, which means that the block might have code that exists 556 | # only in one or another interpreter, leading to false positives when analysed. 557 | analyse-fallback-blocks=no 558 | 559 | # Deprecated modules which should not be used, separated by a comma. 560 | deprecated-modules=optparse,tkinter.tix 561 | 562 | # Create a graph of external dependencies in the given file (report RP0402 must 563 | # not be disabled). 564 | ext-import-graph= 565 | 566 | # Create a graph of every (i.e. internal and external) dependencies in the 567 | # given file (report RP0402 must not be disabled). 568 | import-graph= 569 | 570 | # Create a graph of internal dependencies in the given file (report RP0402 must 571 | # not be disabled). 572 | int-import-graph= 573 | 574 | # Force import order to recognize a module as part of the standard 575 | # compatibility libraries. 576 | known-standard-library= 577 | 578 | # Force import order to recognize a module as part of a third party library. 579 | known-third-party=enchant 580 | 581 | # Couples of modules and preferred modules, separated by a comma. 582 | preferred-modules= 583 | 584 | 585 | [EXCEPTIONS] 586 | 587 | # Exceptions that will emit a warning when being caught. Defaults to 588 | # "BaseException, Exception". 589 | overgeneral-exceptions=builtins.BaseException, 590 | builtins.Exception 591 | -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import octobot_script.cli 18 | 19 | octobot_script.cli.main() 20 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import pytest 18 | import mock 19 | import octobot_script.internal.octobot_mocks as octobot_mocks 20 | 21 | 22 | # only load config once 23 | TEST_CONFIG = octobot_mocks.get_config() 24 | TEST_TENTACLES_CONFIG = octobot_mocks.get_tentacles_config() 25 | 26 | 27 | @pytest.fixture 28 | def mocked_config(): 29 | with mock.patch.object(octobot_mocks, "get_config", mock.Mock(return_value=TEST_CONFIG)), \ 30 | mock.patch.object(octobot_mocks, "get_tentacles_config", mock.Mock(return_value=TEST_TENTACLES_CONFIG)): 31 | yield 32 | -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | -------------------------------------------------------------------------------- /tests/api/test_data_fetching.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import pytest 18 | import mock 19 | 20 | import octobot_commons.enums as commons_enums 21 | import octobot_trading.enums as trading_enums 22 | import octobot_backtesting.api as backtesting_api 23 | import octobot_script as obs 24 | import octobot_script.api.data_fetching as data_fetching 25 | 26 | from tests import mocked_config, TEST_CONFIG, TEST_TENTACLES_CONFIG 27 | 28 | 29 | # All test coroutines will be treated as marked. 30 | pytestmark = pytest.mark.asyncio 31 | 32 | 33 | async def test_historical_data(): 34 | with mock.patch.object(backtesting_api, "initialize_and_run_data_collector", mock.AsyncMock(return_value="data")) \ 35 | as initialize_and_run_data_collector_mock: 36 | assert await obs.historical_data("BTC/USDT", commons_enums.TimeFrames.ONE_DAY.value) == "data" 37 | initialize_and_run_data_collector_mock.assert_awaited_once() 38 | 39 | 40 | async def test_get_data(mocked_config): 41 | with mock.patch.object(data_fetching, "historical_data", 42 | mock.AsyncMock(return_value="data")) as historical_data_mock, \ 43 | mock.patch.object(backtesting_api, "create_and_init_backtest_data", 44 | mock.AsyncMock(return_value="backtest_data")) as create_and_init_backtest_data_mock: 45 | assert await obs.get_data("BTC/USDT", commons_enums.TimeFrames.ONE_DAY.value) == "backtest_data" 46 | historical_data_mock.assert_awaited_once_with( 47 | "BTC/USDT", 48 | timeframe=commons_enums.TimeFrames.ONE_DAY.value, 49 | exchange="binance", 50 | exchange_type=trading_enums.ExchangeTypes.SPOT.value, 51 | start_timestamp=None, 52 | end_timestamp=None, 53 | ) 54 | create_and_init_backtest_data_mock.assert_awaited_once_with( 55 | ["data"], 56 | TEST_CONFIG, 57 | TEST_TENTACLES_CONFIG, 58 | use_accurate_price_time_frame=True 59 | ) 60 | historical_data_mock.reset_mock() 61 | create_and_init_backtest_data_mock.reset_mock() 62 | assert await obs.get_data("BTC/USDT", commons_enums.TimeFrames.ONE_DAY.value, data_file="existing_file") \ 63 | == "backtest_data" 64 | historical_data_mock.assert_not_awaited() 65 | create_and_init_backtest_data_mock.assert_awaited_once_with( 66 | ["existing_file"], 67 | TEST_CONFIG, 68 | TEST_TENTACLES_CONFIG, 69 | use_accurate_price_time_frame=True 70 | ) 71 | 72 | -------------------------------------------------------------------------------- /tests/api/test_execution.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import pytest 18 | import mock 19 | 20 | import octobot_script as obs 21 | import octobot_script.internal.logging_util as logging_util 22 | import octobot_script.internal.runners as runners 23 | 24 | 25 | # All test coroutines will be treated as marked. 26 | pytestmark = pytest.mark.asyncio 27 | 28 | 29 | async def test_run(): 30 | with mock.patch.object(logging_util, "load_logging_config", mock.Mock()) as load_logging_config_mock, \ 31 | mock.patch.object(runners, "run", mock.AsyncMock(return_value="ret")) as run_mock: 32 | def up_func(): 33 | pass 34 | assert await obs.run("backtesting_data", up_func, "strat_config") == "ret" 35 | load_logging_config_mock.assert_not_called() 36 | run_mock.assert_awaited_once_with("backtesting_data", up_func, "strat_config", 37 | enable_logs=False, enable_storage=True) 38 | load_logging_config_mock.reset_mock() 39 | run_mock.reset_mock() 40 | assert await obs.run("backtesting_data", up_func, "strat_config", enable_logs=True, enable_storage=False) \ 41 | == "ret" 42 | load_logging_config_mock.assert_called_once() 43 | run_mock.assert_awaited_once_with("backtesting_data", up_func, "strat_config", 44 | enable_logs=True, enable_storage=False) 45 | -------------------------------------------------------------------------------- /tests/functionnal/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import pytest_asyncio 18 | import os 19 | import octobot_script as obs 20 | 21 | 22 | # only load config once 23 | BACKTESTING_FILES_DIR = os.path.join("tests", "test_util") 24 | ONE_DAY_BTC_USDT_DATA = os.path.join(BACKTESTING_FILES_DIR, "ExchangeHistoryDataCollector_1673796151.325921.data") 25 | 26 | 27 | @pytest_asyncio.fixture 28 | async def one_day_btc_usdt_data(): 29 | data = None 30 | try: 31 | data = await obs.get_data("BTC/USDT", "1d", start_timestamp=1505606400, data_file=ONE_DAY_BTC_USDT_DATA) 32 | yield data 33 | finally: 34 | if data is not None: 35 | await data.stop() 36 | -------------------------------------------------------------------------------- /tests/functionnal/example_scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | -------------------------------------------------------------------------------- /tests/functionnal/example_scripts/test_precomputed_vs_iteration_rsi.py: -------------------------------------------------------------------------------- 1 | # This file is part of OctoBot-Script (https://github.com/Drakkar-Software/OctoBot-Script) 2 | # Copyright (c) 2023 Drakkar-Software, All rights reserved. 3 | # 4 | # OctoBot is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # OctoBot is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public 15 | # License along with OctoBot-Script. If not, see . 16 | 17 | import pytest 18 | import os 19 | import tulipy 20 | 21 | import octobot_script as obs 22 | from tests.functionnal import one_day_btc_usdt_data 23 | 24 | 25 | # All test coroutines will be treated as marked. 26 | pytestmark = pytest.mark.asyncio 27 | 28 | 29 | async def test_precomputed_vs_iteration_rsi(one_day_btc_usdt_data): 30 | # 1. pre-compute entries at first iteration only 31 | async def _pre_compute_update(ctx): 32 | if run_data["entries"] is None: 33 | closes = await obs.Close(ctx, max_history=True) 34 | times = await obs.Time(ctx, max_history=True, use_close_time=True) 35 | rsi_v = tulipy.rsi(closes, period=ctx.tentacle.trading_config["period"]) 36 | delta = len(closes) - len(rsi_v) 37 | run_data["entries"] = { 38 | times[index + delta] 39 | for index, rsi_val in enumerate(rsi_v) 40 | if rsi_val < ctx.tentacle.trading_config["rsi_value_buy_threshold"] 41 | } 42 | await obs.plot_indicator(ctx, "RSI", times[delta:], rsi_v, run_data["entries"]) 43 | if obs.current_live_time(ctx) in run_data["entries"]: 44 | await obs.market(ctx, "buy", amount="10%", stop_loss_offset="-15%", take_profit_offset="25%") 45 | run_data = { 46 | "entries": None, 47 | } 48 | config = { 49 | "period": 10, 50 | "rsi_value_buy_threshold": 28, 51 | } 52 | res = await obs.run( 53 | one_day_btc_usdt_data, _pre_compute_update, config, 54 | enable_logs=False, enable_storage=True 55 | ) 56 | # ensure run happened 57 | assert res.backtesting_data is not None 58 | assert res.strategy_config is not None 59 | assert res.independent_backtesting is not None 60 | assert res.bot_id is not None 61 | assert res.report['bot_report']['profitability']['binance'] != 0 62 | assert res.report["bot_report"]['end_portfolio']['binance'] != \ 63 | res.report["bot_report"]['starting_portfolio']['binance'] 64 | assert res.duration < 10 65 | assert res.candles_count == 1947 66 | await _check_report(res) 67 | 68 | # ensure second run gives the same result 69 | run_data = { 70 | "entries": None, 71 | } 72 | res_2 = await obs.run( 73 | one_day_btc_usdt_data, _pre_compute_update, config, 74 | enable_logs=True, enable_storage=False 75 | ) 76 | assert res_2.bot_id != res.bot_id 77 | assert res_2.report['bot_report']['profitability'] == res.report['bot_report']['profitability'] 78 | assert res_2.report["bot_report"]['end_portfolio']['binance'] != \ 79 | res_2.report["bot_report"]['starting_portfolio']['binance'] 80 | 81 | # try with different config 82 | run_data = { 83 | "entries": None, 84 | } 85 | config = { 86 | "period": 10, 87 | "rsi_value_buy_threshold": 10, 88 | } 89 | res_3 = await obs.run( 90 | one_day_btc_usdt_data, _pre_compute_update, config, 91 | enable_logs=False, enable_storage=False 92 | ) 93 | assert res_3.bot_id is not None 94 | assert res_3.bot_id != res.bot_id 95 | assert res_3.report['bot_report']['profitability'] != res.report['bot_report']['profitability'] 96 | assert res_3.report["bot_report"]['end_portfolio']['binance'] != \ 97 | res_3.report["bot_report"]['starting_portfolio']['binance'] 98 | 99 | # 2. iteration computed entries at each iteration 100 | async def _iterations_update(ctx): 101 | if obs.current_live_time(ctx) != await obs.current_candle_time( 102 | ctx, use_close_time=True): 103 | return 104 | close = await obs.Close(ctx) 105 | if len(close) <= ctx.tentacle.trading_config["period"]: 106 | return 107 | rsi_v = tulipy.rsi(close, period=ctx.tentacle.trading_config["period"]) 108 | if rsi_v[-1] < ctx.tentacle.trading_config["rsi_value_buy_threshold"]: 109 | await obs.market(ctx, "buy", amount="10%", stop_loss_offset="-15%", take_profit_offset="25%") 110 | 111 | res_iteration = await obs.run( 112 | one_day_btc_usdt_data, _iterations_update, config, 113 | enable_logs=False, enable_storage=False 114 | ) 115 | # same result as pre_computed with the same config 116 | assert res_iteration.report['bot_report']['profitability'] == res_3.report['bot_report']['profitability'] 117 | 118 | 119 | async def _check_report(res): 120 | description = res.describe() 121 | assert str(res.strategy_config) in description 122 | report = "report.html" 123 | await res.plot(report_file=report, show=False) 124 | with open(report) as rep: 125 | report_content = rep.read() 126 | for key, val in res.strategy_config.items(): 127 | assert str(key) in report_content 128 | assert str(val) in report_content 129 | assert "BTC/USDT" in report_content 130 | assert "1d" in report_content 131 | assert "Binance" in report_content 132 | os.remove(report) 133 | -------------------------------------------------------------------------------- /tests/test_util/ExchangeHistoryDataCollector_1673796151.325921.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drakkar-Software/OctoBot-Script/8dbab5bf05abc21b78f3a962319d3f67fa232048/tests/test_util/ExchangeHistoryDataCollector_1673796151.325921.data --------------------------------------------------------------------------------